Skip to content
UI

Input OTP

A row of single-character boxes for entering a one-time code, such as a verification or recovery code.

@zenoxai/ui/components/input-otp Markdown
<InputOTP maxLength={6} aria-label="Verification code">
  <InputOTPGroup>
    <InputOTPSlot index={0} />
    <InputOTPSlot index={1} />
    <InputOTPSlot index={2} />
  </InputOTPGroup>
  <InputOTPSeparator />
  <InputOTPGroup>
    <InputOTPSlot index={3} />
    <InputOTPSlot index={4} />
    <InputOTPSlot index={5} />
  </InputOTPGroup>
</InputOTP>

Usage

import {
  InputOTP,
  InputOTPGroup,
  InputOTPSlot,
} from "@zenoxai/ui/components/input-otp"

<InputOTP maxLength={6} aria-label="Verification code">
  <InputOTPGroup>
    <InputOTPSlot index={0} />
    <InputOTPSlot index={1} />
    <InputOTPSlot index={2} />
    <InputOTPSlot index={3} />
    <InputOTPSlot index={4} />
    <InputOTPSlot index={5} />
  </InputOTPGroup>
</InputOTP>

Use it only for short codes the user copies from somewhere else: an email, an SMS, an authenticator app. For anything else, use an Input.

InputOTP is built on the input-otp package. A single transparent <input> sits over the boxes and holds the whole value; the slots only draw it. Render one InputOTPSlot per character, with index from 0 to maxLength - 1, and split them into InputOTPGroups with an InputOTPSeparator when the code reads better in parts.

Props of the underlying <input> pass through: id, name, disabled, autoComplete, inputMode, aria-*. value and onChange(value) work with the full string, and onComplete fires when the last character is entered.

Examples

Digits only

pattern takes a regular expression as a string; input that does not match is ignored, including pasted text. Pair it with inputMode="numeric" for the number keypad on phones and autoComplete="one-time-code" so the platform can offer the code from an SMS.

The 6-digit code from your authenticator app.

<Field className="w-fit">
  <FieldLabel htmlFor="mfa-code">Authenticator code</FieldLabel>
  <InputOTP
    id="mfa-code"
    maxLength={6}
    pattern="^\d+$"
    inputMode="numeric"
    autoComplete="one-time-code"
  >
    <InputOTPGroup>
      <InputOTPSlot index={0} />
      <InputOTPSlot index={1} />
      <InputOTPSlot index={2} />
      <InputOTPSlot index={3} />
      <InputOTPSlot index={4} />
      <InputOTPSlot index={5} />
    </InputOTPGroup>
  </InputOTP>
  <FieldDescription>
    The 6-digit code from your authenticator app.
  </FieldDescription>
</Field>

Controlled

onChange receives the whole code as a string. Verify it in onComplete or once its length reaches maxLength.

6 characters left.

function InputOTPControlled() {
  const [code, setCode] = React.useState("")

  return (
    <div className="grid justify-items-center gap-3">
      <InputOTP
        maxLength={6}
        value={code}
        onChange={setCode}
        aria-label="Recovery code"
      >
        <InputOTPGroup>
          <InputOTPSlot index={0} />
          <InputOTPSlot index={1} />
          <InputOTPSlot index={2} />
          <InputOTPSlot index={3} />
          <InputOTPSlot index={4} />
          <InputOTPSlot index={5} />
        </InputOTPGroup>
      </InputOTP>
      <p className="text-sm text-text-secondary">
        {code.length === 6
          ? `Checking ${code}...`
          : `${6 - code.length} characters left.`}
      </p>
    </div>
  )
}

Invalid

Set aria-invalid on InputOTP: the input announces it and every slot shows the destructive border. Put the message in a FieldError.

4
8
2
9
1
3
<Field data-invalid className="w-fit">
  <FieldLabel htmlFor="otp-invalid">Verification code</FieldLabel>
  <InputOTP
    id="otp-invalid"
    maxLength={6}
    defaultValue="482913"
    aria-invalid
    aria-describedby="otp-invalid-error"
  >
    <InputOTPGroup>
      <InputOTPSlot index={0} />
      <InputOTPSlot index={1} />
      <InputOTPSlot index={2} />
      <InputOTPSlot index={3} />
      <InputOTPSlot index={4} />
      <InputOTPSlot index={5} />
    </InputOTPGroup>
  </InputOTP>
  <FieldError id="otp-invalid-error">
    This code has expired. Request a new one.
  </FieldError>
</Field>

Disabled

1
2
3
4
<InputOTP
  maxLength={6}
  defaultValue="1234"
  disabled
  aria-label="Verification code"
>
  <InputOTPGroup>
    <InputOTPSlot index={0} />
    <InputOTPSlot index={1} />
    <InputOTPSlot index={2} />
    <InputOTPSlot index={3} />
    <InputOTPSlot index={4} />
    <InputOTPSlot index={5} />
  </InputOTPGroup>
</InputOTP>

Accessibility

  • Screen readers see one text input, not six boxes. Name it with aria-label or a FieldLabel pointing at its id.
  • Paste fills every slot at once, and password managers can fill the code into the hidden input.
  • Say in the label or description how many characters the code has and where to find it.

API reference

Generated from the TypeScript source. className, style and children are accepted everywhere and omitted below.

InputOTP

InputOTP is a one-time code field with a box per character, built on input-otp; it accepts a pasted code and the browser's one-time-code autofill. Set `maxLength` to the code length and render that many InputOTPSlot elements, grouped with InputOTPGroup.

PropTypeDefault
containerClassNamestring

Classes for the wrapper around the slots; `className` styles the input.

-

InputOTPGroup

InputOTPGroup joins adjacent slots into one bordered segment.

No props of its own. Accepts the props of the element it renders.

InputOTPSlot

InputOTPSlot is the box that shows one character of the code.

PropTypeDefault
index*number

Position of the character it shows, from 0.

-

InputOTPSeparator

InputOTPSeparator is the dash between two groups of slots.

No props of its own. Accepts the props of the element it renders.