Input OTP

A one-time-code input; a row of single-character slots for verification and PIN codes, compose inside a Field.

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

Usage

A row of single-character slots for entering a one-time code. Compose it inside a Field for the label, description, and error wiring.

import { InputOtp } from '@epilot/spark-ui/input-otp'
function VerifyStep() {
  return (
    <Field>
      <FieldLabel>Verification code</FieldLabel>
      <InputOtp onComplete={(code) => submit(code)} />
    </Field>
  )
}

Typing advances to the next slot, backspace moves back, and pasting a full code fills every slot at once. Use it for short verification or PIN codes; for a plain value use Input, and for a value with a leading or trailing addon use Input Group.

Examples

Centered verification screen

The usual OTP layout centers everything. text-center on the Field centers the label and description; justify-center on the control centers the slot row. No dedicated prop needed.

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

Length

length sets the slot count (default 6). A 4-slot field suits a shorter PIN.

Invalid

A FieldError inside the Field switches every slot to the error palette and marks the first slot aria-invalid, matching where the aria-describedby link lands. Set data-invalid on the Field yourself only for the invalid look without a message.

Disabled

Set data-disabled on the Field and disabled on the control. All slots gray out and stop accepting input; the label keeps full contrast.

API Reference

InputOtp

Renders Base UI OTP Field. The style and color follow the enclosing Field, else the data-input-style (soft) and data-input-color (brand) knobs, so the slots match the rest of the input family. autoComplete="one-time-code" is on by default, and validationType decides which characters the slots accept and which keyboard mobile shows — it defaults to alphanumeric, so digits-only flows opt in with validationType="numeric" to get the numeric keypad.

PropTypeDefaultDescription
lengthnumber6Number of code slots.
validationType"numeric" | "alpha" | "alphanumeric" | "none""alphanumeric"Characters the slots accept; others are dropped on typing and paste. numeric is the only value that sets the numeric keypad — pass it for digits-only codes; alpha/alphanumeric use a text keyboard; none filters nothing and sets no inputMode.
valuestringControlled value. Provide onValueChange to update it.
defaultValuestringInitial value when uncontrolled.
onValueChange(value: string, details) => voidFires on every edit (typing, paste, clear); details.reason names what triggered it.
onComplete(value: string, details) => voidFires when the last empty slot is filled. Maps to Base UI onValueComplete.
slotAriaLabel(index: number) => string`Character ${index + 1}`Accessible name per slot (override to localize). The first slot is named by the Field label.
disabledbooleanfalseGrays out every slot and ignores input.
idstringFieldId of the first slot; the rest derive from it. Defaults to the enclosing Field's id.
classNamestringMerges onto the slot row (the Base UI OTPField.Root).

Field state attributes

Set on Field; the composition reacts as a whole.

AttributePair withEffect
data-invalidFieldError for messageset by a rendered FieldError; every slot turns error red
data-disableddisabled on InputOtpSlots gray out; the label keeps full contrast

Accessibility

  • The slots form one role="group" labelled by the FieldLabel; the label names the group and its first slot, and the remaining slots get positional names (Character 2, Character 3, …). Always provide a FieldLabel so the first slot is named.
  • A FieldDescription is wired to the first slot via aria-describedby.
  • Arrow keys move between slots, Home/End jump to the first/last, and pasting a full code distributes it across the slots.

On this page