Field.OneTimeCode

Field.OneTimeCode is a sign-in or verification code inside a Field, drawn as one box per character. Its value is a string of at most length characters, 6 by default. characters is "digits" by default, or "alphanumeric" for letters and digits. Pass onValueChange for the cleaned code, onChange for the event, or both; without either, the input is read-only. onChange gets the native event with the raw text, so a product that uses only onChange cleans it or uses onValueChange. A product that sends the code by itself does so a short pause after value.length === length, since with an IME a full code can be reported and taken back (see Typing, paste and autofill).

On this page
const id = useId();
const hintId = useId();
const [code, setCode] = useState('');

<Field.Group>
  <Label.Root htmlFor={id}>Verification code</Label.Root>
  <Field.Root id={id} name="code" aria-describedby={hintId}>
    <Field.OneTimeCode value={code} onValueChange={setCode} />
  </Field.Root>
  <Description.Root id={hintId}>Enter the 6-digit code we sent.</Description.Root>
</Field.Group>
Enter the 6-digit code we sent.

The boxes are drawn over one native <input>, not made of one input per character. So the browser keeps paste, undo, selection and screen reader support, and a phone can offer the code from a text message because the input has autocomplete="one-time-code". Inputs split per digit break each of these. The boxes are aria-hidden and take no pointer events; the input is the control.

Typing, paste and autofill

One rule decides what counts: a character is kept when characters allows it, and full-width digits and letters, which an East Asian keyboard can type, count as their ASCII ones. Other characters that only look like digits or letters, such as ½, ① or ², are refused. Typing, paste, a drop, autofill and an IME composition all follow it.

  • A character typed into a digits code that is not a digit is refused, and the same goes for a character outside letters and digits in an alphanumeric one. Pasting 123 456 or Your code: 482-913 gives 123456 and 482913: the characters the code doesn't allow are dropped, and a paste with no allowed character does nothing, as does a paste your onPaste cancels with preventDefault. A keyboard's code suggestion or voice input that inserts more than one character at once is treated as a paste.
  • One rule decides where the text lands. A whole code replaces the value, so pasting 123456 into 12 gives 123456, not 121234. Anything shorter replaces the selection and writes over the characters after it as far as it needs room, and what still doesn't fit is cut, so nothing already typed is pushed off the end of the code. On a code that is not full, typing pushes the rest right, as in any input. On a full code, typing at a box replaces it, and pasting 78 at the third box of 123456 gives 127856. With the caret after a full code, nothing lands: typing 12345678 into an empty code leaves 123456, so a product that sends a full code never gets a second, different code from an extra key.
  • A drop lands at the box it is dropped on, or after the code when it is dropped past the last box, not at the point on the invisible input's text that the browser picks. Text dragged from inside the field is copied, not moved, and a drop your onDrop cancels with preventDefault lands nothing.
  • An IME composition is the browser's while it is composed, and the boxes show it as it will land. A composed text that is already a valid code reaches onChange and onValueChange as it is typed, as a phone keyboard may compose a whole code as one word. So a romaji or pinyin IME can report a Latin letter such as k as part of a code and take it back when the next step, か, replaces it; the code before it is then reported again when the composition ends, to a product that normalizes the code (any change of case in value, whenever it arrives) or holds it fixed too. So with an IME a full code can be reported mid-composition and taken back: a product that sends by itself waits a short pause after the code is complete, and keeps a button to send it. Anything else is applied when the composition ends: the same rule applies the text it left in the input, the text the boxes showed. Composed text the code doesn't allow, or a cancelled composition, leaves the code as it was before the composition: onValueChange doesn't run, or, when the composition's steps were already reported, gets that code back. A value your code sets while a composition runs wins, and the composition is dropped, unless it is a code Todae reported during that composition, in any case. Deleting is the browser's.
  • Todae puts each edit in with document.execCommand, so it is one step on the input's undo stack, and Ctrl+Z undoes typing and paste. After a composition, undo may also take back what was typed just before it, or leave the code as it is. An undo or redo step that is no code is refused, so onValueChange never gets a code the person didn't see. Where execCommand is not available, it writes the value itself and fires an input event, so onChange runs as it does for typing.
  • onValueChange always gets the cleaned code, and runs only when the code changes. An autofill, or an insert the browser won't let be cancelled, can put text the rule never saw into the input, and the change event reads it by the same rule.

Pressing a box

A press on a box selects its character, so typing replaces it. A press on an empty box, or anywhere past the last box, puts the caret after the code: the code fills the field's row, with the boxes at its start, left to right in a right-to-left page too. There is room past the last box only when the field's row is wider than the boxes; in the form example below, under Stack.Root align="start", the row is as wide as the boxes. A drag or a Shift+click keeps the range it selected, and a double click still selects the whole code. With the caret after a full code, the focus ring goes around the row of boxes, in no box. The arrow keys, Home, End and Backspace are the input's own, and the active box follows the caret: the box at the caret, or the boxes of a selection. An empty box that waits for the next character shows a blinking caret, steady under prefers-reduced-motion: reduce. The focus ring goes on the active boxes, not around the field.

In a form

The input is the field's native, validated control, so it sends its text under the field's name and takes required and the other native attributes. It has maxLength equal to length. A code that is not empty and shorter than length is flagged as too short, whether a person typed it or your code set it, because the browser's own tooShort skips a value a script writes. An empty code is the job of required. The problem has kind: 'customError', not 'tooShort', and its message is the tooShort string. While the code is short, that message wins over a Field.Root customValidity, since the innermost owner comes first. Inside a Form.Root the problem shows in the field's ErrorMessage.Root in the page's language, such as "Use at least 8 characters", and focus goes to the input.

const id = useId();
const [code, setCode] = useState('');

<Form.Root onSubmit={verify}>
  <Stack.Root align="start">
    <Field.Group>
      <Label.Root htmlFor={id}>Recovery code</Label.Root>
      <Field.Root id={id} name="recovery">
        <Field.OneTimeCode required length={8} characters="alphanumeric" value={code} onValueChange={setCode} />
      </Field.Root>
      <ErrorMessage.Root />
    </Field.Group>
    <Button.Root type="submit">Verify</Button.Root>
  </Stack.Root>
</Form.Root>

pattern is [0-9]* or [A-Za-z0-9]*. It leaves length to the too-short check, so a code that is too short reads "too short" and not "wrong format".

Accessible composition

Todae handles: The input takes id, name, aria-label, aria-labelledby and aria-describedby from Field.Root, and native disabled and aria-invalid from the Root or a surrounding Fieldset. It is a text input with autocomplete="one-time-code", a numeric keypad for digits, spell checking, autocorrect and capitalization off, and dir="ltr", since codes are Latin characters. The boxes are aria-hidden, so a screen reader reads the input's own text.

You supply: A name, through a Label whose htmlFor is the Root id or aria-label / aria-labelledby on the Root. The code's length in a Description, such as "Enter the 6-digit code we sent". The Form.Root or onValueChange that sends the code, and any resend control.

Watch for: There is no onComplete: send the code from onValueChange a short pause after value.length === length, and keep a submit button for people who don't expect it to send by itself. A value that holds characters the code doesn't allow is shown as it is, so clean what you set. With an IME on, as for an alphanumeric code, composed text outside the set leaves the code as it was, so the person switches to a Latin keyboard. A code that is not a one-time code, such as a postal code or an ID, is better as a plain input.

When to use

Field.OneTimeCode

When: A sign-in, two-factor or verification code of a fixed length that people type, paste or autofill from a message.

When not: A code that has no fixed length, or any text → Field.TextInput. A number to step or format → Field.NumberInput. A separate input per digit, which breaks paste, autofill and screen readers.

Props

Field.OneTimeCode props
NameTypeDescription
value Requiredstring

The code: at most length characters of the allowed set.

onChange(event: ChangeEvent<HTMLInputElement>) => void

Optional: onValueChange alone is enough when the event is not needed. The event holds the raw text; clean it, or use onValueChange.

onValueChange(value: string) => void

Called with the code cleaned to the allowed characters and cut to length. Without it and onChange the input is read-only.

lengthnumber

How many characters the code has, and how many boxes are drawn.

Default: 6

characters"digits" | "alphanumeric"

digits asks the phone for a numeric keypad and refuses other characters as they are typed.

Default: "digits"

classNamestring

On the element that draws the boxes, not on the input, which is transparent.

styleCSSProperties

On the element that draws the boxes, like className.

autoCompletestring

Lets the phone offer the code from a text message.

Default: "one-time-code"