Noctis

OTP field

Enter a one-time verification code across a row of single-character cells that behave as one input. Typing advances to the next cell, a pasted code fills the whole field at once, and the focused cell takes the accent ring.

Basic

Give OtpField.Root a length and render that many OtpField.Input cells inside. Label the field with a single <label htmlFor> pointed at the root's id — Base UI wires the first cell to it and names the group for assistive tech — then point aria-describedby at a short instruction so people know what code to enter and how many digits it is.

A filled cell firms its border so entered digits read as present while the empty cells stay quieter. The field stays neutral when it fills — a full field is not a verified code, so success is left to the surrounding form rather than signalled prematurely.

Enter the 6-digit code we sent to your phone.

Grouped

The canonical six-digit code is split into two groups of three — 123-456. Interleave an OtpField.Separator between the two runs of cells to teach that grouping; it renders a thin neutral divider and sits between the logical groups, so the split mirrors correctly under RTL. Grouping is composition, not a prop — render the separator wherever a break belongs.

Sizes

Two heights — medium and large — sharing the field surface and the control-height ladder. Each cell is a square 1:1 slot whose width tracks its height, and the character size tracks the cell.

Custom size

md and lg are the built-in steps, but OTP cells are often wanted bigger than either. The cell's size is a minted seam — set --noctis-otp-field-input-inline-size, --noctis-otp-field-input-block-size, and --noctis-otp-field-input-font-size on any ancestor (here a typed style object; a real consumer writes a CSS rule on a wrapper class) and every cell beneath retunes. Width and height are independent, so keep them equal for a square cell.

Masked

Set mask on OtpField.Root to obscure entered characters — for a PIN or any secret that should not read on screen.

Controlled

Drive the value yourself with value and onValueChange on OtpField.Root — for validating, persisting, or reacting to each edit. onValueComplete fires when every cell is filled, the moment to verify the code. Omit them (use defaultValue) to let the component own its state.

Value:

Verify on complete

A verification field is a submit-on-complete control: validate the instant the last digit lands instead of asking for an extra tap. Set autoSubmit to submit the owning form the moment the field fills, and use onValueComplete to kick off the check and show a pending state.

Fills and verifies the moment all six digits are entered.

In a form

Wrap the field in a Field.Root for the full label, instruction, and error contract. Field.Label names the group, Field.Description wires the instruction through aria-describedby, and reporting the field invalid (here from a wrong code) flows data-invalid onto every cell — lighting the danger border — while Field.Error announces the message.

Enter the 6-digit code we sent to your phone.

Mobile & autofill

OTP UX lives or dies on the phone. Base UI sets the pieces up for you and exposes the knobs:

  • SMS autofill. The first cell carries autoComplete="one-time-code" (the default) plus a hidden validation input, so iOS and Android offer the code from the SMS and fill the whole field. This is why the field must stay one Base UI OtpField — splitting it into independent <input>s breaks one-time-code autofill and paste.
  • Numeric keyboard. With the default validationType="numeric", inputMode resolves to numeric, so phones raise the number pad. Override inputMode when you accept letters (see below).
  • WebOTP. Browsers that support the WebOTP API can read the code from the SMS and submit it automatically — a progressive enhancement on top of the same markup; pair it with autoSubmit.
  • Caveat. A password manager may overlay its badge on the first cell or two. It is harmless, but worth knowing when you size the field.

Validation & restriction

validationType decides which characters a cell accepts: numeric (default), alpha, alphanumeric, or none. Rejected keystrokes are dropped and reported through onValueInvalid. Use normalizeValue to canonicalize as the user types — uppercasing an alphanumeric coupon, say — and switch inputMode to text so the full keyboard shows.

On a surface

The field sits on whatever surface hosts it. On an elevated card the focus ring, the firmed filled cells, and the native accent caret all read clearly against the raised background.

Keyboard

The row behaves as one field. Movement is RTL-aware — in a right-to-left layout the arrow keys mirror.

KeyAction
CharactersFill the active cell and advance to the next.
BackspaceClear the active cell, or move to the previous cell when empty.
/ Move to the previous / next cell (mirrored under RTL).
Home / EndJump to the first / last cell.
PasteFill the whole field from the clipboard at once.

Accessibility

  • Name the group once. A single <label htmlFor> on the root id (or Field.Label) names the field; Base UI wires the first cell to it. Do not also label the first cell — give cells 2…n an aria-label that includes the total, like Digit 3 of 6, so a screen-reader user always knows where they are.
  • Instruct with aria-describedby. Point it at a short instruction ("Enter the 6-digit code we sent to your phone"). Inside a Field.Root, Field.Description does this for you.
  • Announce errors. Report the field invalid (or wrap in Field.Root and use Field.Error) so each cell gets data-invalid and the message is announced.
  • One field, not many. Keep the cells inside one OtpField so paste, cut, select-all, and one-time-code autofill keep working — the accessible recommendation for split code inputs.
  • The caret is real. Each cell is a genuine <input>, so the active empty cell shows the browser's own blinking caret (tinted to the accent); it honours the user's system caret and reduced-motion settings with no extra markup.

Anatomy

Compose the field from its parts. OtpField.Root owns the value (controlled via value / onValueChange, or uncontrolled via defaultValue), the length, and the shared size.

  • OtpField.Root — the container. Props: length (required), size (default md), mask, autoSubmit, validationType (default numeric), inputMode, autoComplete (default one-time-code), normalizeValue, required, readOnly, disabled, plus the Base UI value props (value, defaultValue, onValueChange, onValueComplete, onValueInvalid). Give it an id and pair it with a <label htmlFor>.
  • OtpField.Input — one character cell. Render length of them inside the root; Base UI manages focus, typing, and paste across the row.
  • OtpField.Separator — the divider between cell groups. Render it between two runs of cells to teach the 123-456 grouping; orientation flows through to Base UI.

Every part carries a data-slot (otp-field, otp-field-input, otp-field-separator) for host-side styling — pair it with the Base UI state attributes (data-complete, data-filled, data-focused, data-disabled, data-readonly, data-invalid). The row is fully keyboard-operable and RTL-aware.

On surfaces

The same control re-tuned across the elevation scopes — the root canvas, an elevated panel, a menu, and a sunken well. It stays legible on every layer.

root
elevated
menu
sunken

Design tokens

Generated from the component's declaration — the same graph that mints the CSS, so a variable name or its resolution default can't drift. The minted tokens are the public override seam: set one on any ancestor and every OTP field in that region retunes — e.g. .narrow { --noctis-otp-field-input-inline-size: var(--noctis-size-control-sm); } slims the cells from their default square. Knobs that aren't minted are reached through the part's data-slot. See Customization for the full override ladder and Tokens for the whole graph.

Token

API reference

Generated from the component's types — every prop, type, default, and description comes straight from the source. Each part gets its own table; Input and Separator list just the props they forward to Base UI.

OtpField.Root

Prop

OtpField.Input

Prop

OtpField.Separator

Prop

AttributeDescription
data-slotThe root OTP-field element.
data-completePresent on the root when every slot is filled.
data-filledPresent on a cell that contains a character, and on the root when any cell is filled.
data-disabledPresent when the field is disabled.
data-readonlyPresent when the field is read-only.
data-requiredPresent when the field is required.
data-focusedPresent on the root when one of the cells is focused.
data-validPresent when the field is in a valid state (wrapped in `Field.Root`).
data-invalidPresent when the field is in an invalid state (wrapped in `Field.Root`).