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

Set data-invalid on the Field. Every slot switches to the error palette; pair it with a FieldError for the 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" and a numeric keyboard are on by default.

PropTypeDefaultDescription
lengthnumber6Number of code slots.
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`Digit ${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 messageEvery 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 (Digit 2, Digit 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