# Input OTP

A one-time code entered one character per box: typing moves on, Backspace moves back, and a pasted code fills every box.

- **Import:** `import { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot } from "@booleanpress/ui/input-otp"`
- **Also install:** `@base-ui/react`
- **Page:** <https://ui.booleanpress.com/components/input-otp> · @booleanpress/ui 0.2.0

## Usage

`InputOTP` is [Base UI's OTP Field](https://base-ui.com/react/components/otp-field) with shadcn's input-otp parts and the field look. It needs the `@base-ui/react` peer package, which only products that import `@booleanpress/ui/input-otp` install. `maxLength` is the number of characters; render one `InputOTPSlot` for each, with its `index` from 0.

```tsx
import { InputOTP, InputOTPGroup, InputOTPSlot } from "@booleanpress/ui/input-otp"
import { Label } from "@booleanpress/ui/label"

export function VerifyCode() {
  return (
    <>
      <Label htmlFor="code">Verification code</Label>
      <InputOTP id="code" maxLength={6}>
        <InputOTPGroup>
          {Array.from({ length: 6 }, (_, index) => (
            <InputOTPSlot key={index} index={index} />
          ))}
        </InputOTPGroup>
      </InputOTP>
    </>
  )
}
```

It is uncontrolled with `defaultValue`, or controlled with `value` and `onValueChange`; `onValueComplete` is called once every box is filled. By default it takes digits only, as most codes are: the boxes carry `inputmode="numeric"`, so phones open the number pad, and the first box `autocomplete="one-time-code"`, so they offer the code from a text message. `validationType="alphanumeric"` takes letters and digits on the full keyboard, `"alpha"` letters only and `"none"` anything. Unlike shadcn's, there is no `pattern` regular expression: use `validationType`, or `normalizeValue` to change what is typed (for example to upper case). `mask` hides the characters as a password field does. `size` is `sm` (28 px), `default` (35 px) or `lg` (42 px) and `variant="filled"` fills the boxes grey; both default to the provider's `controlSize` and `fieldVariant`. Split a long code with `InputOTPSeparator` between two `InputOTPGroup`s. With `name`, the code is submitted with its form, and a reset of the form puts an uncontrolled field back to its `defaultValue`; a partly filled code fails the form's check, so the form submits once every box is filled (or none is, unless `required`). `autoSubmit` submits the form once it is complete.

## Examples

### Basic

Six boxes under a label; the first box takes the label's name.

```tsx
import { InputOTP, InputOTPGroup, InputOTPSlot } from "@booleanpress/ui/input-otp"
import { Label } from "@booleanpress/ui/label"

export default function InputOTPBasic() {
  return (
    <div className="flex flex-col items-center gap-2">
      <Label htmlFor="otp-basic">Verification code</Label>
      <InputOTP id="otp-basic" maxLength={6}>
        <InputOTPGroup>
          {Array.from({ length: 6 }, (_, index) => (
            <InputOTPSlot key={index} index={index} />
          ))}
        </InputOTPGroup>
      </InputOTP>
    </div>
  )
}
```

### Controlled

`value` and `onValueChange` hold the code in state; Reset empties it.

```tsx
import { useState } from "react"
import { Button } from "@booleanpress/ui/button"
import { InputOTP, InputOTPGroup, InputOTPSlot } from "@booleanpress/ui/input-otp"
import { Label } from "@booleanpress/ui/label"

export default function InputOTPControlled() {
  const [value, setValue] = useState("")

  return (
    <div className="flex flex-col items-center gap-4">
      <div className="flex flex-col items-center gap-2">
        <Label htmlFor="otp-controlled">Two-step code</Label>
        <InputOTP id="otp-controlled" maxLength={4} value={value} onValueChange={setValue}>
          <InputOTPGroup>
            {Array.from({ length: 4 }, (_, index) => (
              <InputOTPSlot key={index} index={index} />
            ))}
          </InputOTPGroup>
        </InputOTP>
      </div>
      <div className="flex items-center gap-3 text-sm/normal text-muted-foreground">
        <span>
          Value: <output className="font-medium text-foreground">{value || "empty"}</output>
        </span>
        <Button size="sm" variant="secondary" onClick={() => setValue("")}>
          Reset
        </Button>
      </div>
    </div>
  )
}
```

### Mask

`mask` hides each character as it is typed.

```tsx
import { InputOTP, InputOTPGroup, InputOTPSlot } from "@booleanpress/ui/input-otp"
import { Label } from "@booleanpress/ui/label"

export default function InputOTPMask() {
  return (
    <div className="flex flex-col items-center gap-2">
      <Label htmlFor="otp-mask">Account PIN</Label>
      <InputOTP id="otp-mask" maxLength={4} mask validationType="numeric">
        <InputOTPGroup>
          {Array.from({ length: 4 }, (_, index) => (
            <InputOTPSlot key={index} index={index} />
          ))}
        </InputOTPGroup>
      </InputOTP>
    </div>
  )
}
```

### Integer only

Digits only, the default (`validationType="numeric"`): letters are refused and phones open the number pad.

```tsx
import { InputOTP, InputOTPGroup, InputOTPSlot } from "@booleanpress/ui/input-otp"
import { Label } from "@booleanpress/ui/label"

export default function InputOTPIntegerOnly() {
  return (
    <div className="flex flex-col items-center gap-2">
      <Label htmlFor="otp-integer">Code from your authenticator app</Label>
      <InputOTP id="otp-integer" maxLength={6} validationType="numeric">
        <InputOTPGroup>
          {Array.from({ length: 6 }, (_, index) => (
            <InputOTPSlot key={index} index={index} />
          ))}
        </InputOTPGroup>
      </InputOTP>
    </div>
  )
}
```

### Letters and digits

`validationType="alphanumeric"` takes letters too, on the full keyboard; `normalizeValue` writes them in capitals.

```tsx
import { InputOTP, InputOTPGroup, InputOTPSlot } from "@booleanpress/ui/input-otp"
import { Label } from "@booleanpress/ui/label"

export default function InputOTPAlphanumeric() {
  return (
    <div className="flex flex-col items-center gap-2">
      <Label htmlFor="otp-alphanumeric">Recovery code</Label>
      <InputOTP
        id="otp-alphanumeric"
        maxLength={6}
        validationType="alphanumeric"
        normalizeValue={(value) => value.toUpperCase()}
      >
        <InputOTPGroup>
          {Array.from({ length: 6 }, (_, index) => (
            <InputOTPSlot key={index} index={index} />
          ))}
        </InputOTPGroup>
      </InputOTP>
    </div>
  )
}
```

### With separator

Two groups of three with `InputOTPSeparator` between them; the code is still one value.

```tsx
import { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot } from "@booleanpress/ui/input-otp"
import { Label } from "@booleanpress/ui/label"

export default function InputOTPWithSeparator() {
  return (
    <div className="flex flex-col items-center gap-2">
      <Label htmlFor="otp-separator">Recovery code</Label>
      <InputOTP id="otp-separator" maxLength={6}>
        <InputOTPGroup>
          <InputOTPSlot index={0} />
          <InputOTPSlot index={1} />
          <InputOTPSlot index={2} />
        </InputOTPGroup>
        <InputOTPSeparator />
        <InputOTPGroup>
          <InputOTPSlot index={3} />
          <InputOTPSlot index={4} />
          <InputOTPSlot index={5} />
        </InputOTPGroup>
      </InputOTP>
    </div>
  )
}
```

### Sizes

`sm` boxes are 28 px, `default` 35 px and `lg` 42 px.

```tsx
import { InputOTP, InputOTPGroup, InputOTPSlot } from "@booleanpress/ui/input-otp"

const SIZES = [
  { size: "sm", label: "Small code" },
  { size: "default", label: "Default code" },
  { size: "lg", label: "Large code" },
] as const

export default function InputOTPSizes() {
  return (
    <div className="flex flex-col items-center gap-3">
      {SIZES.map(({ size, label }) => (
        <InputOTP key={size} maxLength={4} size={size} aria-label={label}>
          <InputOTPGroup>
            {Array.from({ length: 4 }, (_, index) => (
              <InputOTPSlot key={index} index={index} />
            ))}
          </InputOTPGroup>
        </InputOTP>
      ))}
    </div>
  )
}
```

### Filled

`variant="filled"` fills the boxes with the grey `--field-filled`.

```tsx
import { InputOTP, InputOTPGroup, InputOTPSlot } from "@booleanpress/ui/input-otp"
import { Label } from "@booleanpress/ui/label"

export default function InputOTPFilled() {
  return (
    <div className="flex flex-col items-center gap-2">
      <Label htmlFor="otp-filled">Verification code</Label>
      <InputOTP id="otp-filled" maxLength={4} variant="filled">
        <InputOTPGroup>
          {Array.from({ length: 4 }, (_, index) => (
            <InputOTPSlot key={index} index={index} />
          ))}
        </InputOTPGroup>
      </InputOTP>
    </div>
  )
}
```

### Disabled

`disabled` greys every box and takes the field out of the tab order.

```tsx
import { InputOTP, InputOTPGroup, InputOTPSlot } from "@booleanpress/ui/input-otp"
import { Label } from "@booleanpress/ui/label"

export default function InputOTPDisabled() {
  return (
    <div className="flex flex-col items-center gap-2">
      <Label htmlFor="otp-disabled">Verification code</Label>
      <InputOTP id="otp-disabled" maxLength={4} disabled>
        <InputOTPGroup>
          {Array.from({ length: 4 }, (_, index) => (
            <InputOTPSlot key={index} index={index} />
          ))}
        </InputOTPGroup>
      </InputOTP>
    </div>
  )
}
```

### Invalid

`aria-invalid` draws the error edge on every box; `aria-describedby` reads the message with the field.

```tsx
import { InputOTP, InputOTPGroup, InputOTPSlot } from "@booleanpress/ui/input-otp"
import { Label } from "@booleanpress/ui/label"

export default function InputOTPInvalid() {
  return (
    <div className="flex flex-col items-center gap-2">
      <Label htmlFor="otp-invalid">Verification code</Label>
      <InputOTP id="otp-invalid" maxLength={6} defaultValue="482913" aria-invalid aria-describedby="otp-invalid-error">
        <InputOTPGroup>
          {Array.from({ length: 6 }, (_, index) => (
            <InputOTPSlot key={index} index={index} />
          ))}
        </InputOTPGroup>
      </InputOTP>
      <p id="otp-invalid-error" className="text-xs/normal text-destructive-strong">
        This code has expired. Ask for a new one.
      </p>
    </div>
  )
}
```

### Sample

A "Verify your email" card: large boxes for digits, a resend link and a button that waits for the whole code.

```tsx
import { useState } from "react"
import { Button } from "@booleanpress/ui/button"
import { InputOTP, InputOTPGroup, InputOTPSlot } from "@booleanpress/ui/input-otp"

export default function InputOTPSample() {
  const [code, setCode] = useState("")

  return (
    <div className="mx-auto flex w-full max-w-xs flex-col gap-4 rounded-xl border bg-card p-6 shadow-sm">
      <div className="flex flex-col gap-1">
        <h3 id="verify-title" className="text-lg/normal font-semibold">
          Verify your email
        </h3>
        <p className="text-sm/normal text-muted-foreground">Enter the 6-digit code we sent to admin@example.com.</p>
      </div>
      <InputOTP
        maxLength={6}
        size="lg"
        validationType="numeric"
        value={code}
        onValueChange={setCode}
        aria-labelledby="verify-title"
        className="justify-between"
      >
        <InputOTPGroup className="w-full justify-between">
          {Array.from({ length: 6 }, (_, index) => (
            <InputOTPSlot key={index} index={index} />
          ))}
        </InputOTPGroup>
      </InputOTP>
      <div className="flex items-center justify-between gap-2 text-sm/normal">
        <span className="text-muted-foreground">Didn’t receive it?</span>
        <Button variant="link" className="h-auto p-0">
          Send again
        </Button>
      </div>
      <Button disabled={code.length < 6}>Verify</Button>
    </div>
  )
}
```

## Accessibility

**Semantics.** A `div` with `role="group"` holds one native `input` per character, with `autocomplete="one-time-code"` on the first, so phones offer the code from a text message. `InputOTPSeparator` is a `role="separator"`. A hidden input carries the whole value for forms.

**Labels.** Name the field with a `Label` whose `htmlFor` is the `InputOTP`'s `id`: it names the group and the first box. The other boxes are named by their position, "Character 2 of 6", from the provider's `otpCharacter` string. Without a visible label, give `InputOTP` an `aria-label`, or `aria-labelledby` pointing at a heading. Put an error message in `aria-describedby`.

**Focus.** The field is one tab stop: Tab enters at the first empty box and the next Tab leaves the field. Each box shows focus by turning its edge `--ring`.

**Known limits.**

- There is no fake caret: each box is a real input with the browser's own caret.
- Each box is 36 × 35 px at the default size, 28 × 28 px small: both meet WCAG 2.5.8's 24 px.

### Keyboard

| Key | Behaviour |
| --- | --- |
| 0–9 | Fills the focused box and moves to the next one. Letters are ignored unless `validationType` takes them (`alphanumeric`, `alpha`). |
| Backspace | Empties the focused box, or the previous one when it is empty, and moves back. |
| Delete | Removes the focused box's character; the ones after it move back. |
| ← | Moves to the previous box (the next one in right-to-left). |
| → | Moves to the next box (the previous one in right-to-left). |
| Home | Moves to the first box. |
| End | Moves to the box after the last character. |
| Ctrl + V | Pastes a code across the boxes from the focused one; spaces and refused characters are dropped. |
| Tab | Leaves the field; the boxes are one tab stop. |

## API

### InputOTP

Renders Base UI OTPField.Root and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `maxLength` (required) | `number` |  | The number of characters, one box each. |
| `autoComplete` | `string` | `one-time-code` | The input autocomplete attribute. Applied to the first slot and hidden validation input. |
| `autoSubmit` | `boolean` | `false` | Whether to submit the owning form when the OTP becomes complete. |
| `className` | `string` |  |  |
| `defaultValue` | `string` |  | The uncontrolled OTP value when the component is initially rendered. |
| `disabled` | `boolean` | `false` | Whether the component should ignore user interaction. |
| `form` | `string` |  | A string specifying the `form` element with which the hidden input is associated. This string's value must match the id of a `form` element in the same document. |
| `id` | `string` |  | The id of the first input element. Subsequent inputs derive their ids from it (`{id}-2`, `{id}-3`, and so on). |
| `inputMode` | `"search" \| "text" \| "none" \| "tel" \| "url" \| "email" \| "numeric" \| "decimal"` |  | The virtual keyboard hint applied to the slot inputs and hidden validation input. Built-in validation modes provide sensible defaults, but you can override them when needed. |
| `mask` | `boolean` | `false` | Whether the slot inputs should mask entered characters. Pass `type` directly to individual `<OTPField.Input>` parts to use a custom input type. |
| `name` | `string` |  | Identifies the field when a form is submitted. |
| `normalizeValue` | `((value: string) => string)` |  | Function that normalizes the OTP value after whitespace and `validationType` filtering. It runs whenever OTP Field normalizes a value, including initial/default values, controlled values, and user edits. The returned value is filtered by `validationType` again, then clamped to `length`. It should be idempotent because OTP Field may normalize the same value more than once while handling edits, storing state, and rendering controlled or uncontrolled values. Non-idempotent normalizers can compound across those normalization passes. Characters rejected while normalizing typed or pasted text are reported through `onValueInvalid`. |
| `onValueChange` | `((value: string, eventDetails: OTPFieldRootChangeEventDetails) => void)` |  | Callback fired when the OTP value changes. The `eventDetails.reason` indicates what triggered the change: - `'input-change'` for typing or autofill - `'input-clear'` when a character is removed by text input - `'input-paste'` for paste interactions - `'keyboard'` for keyboard interactions that change the value |
| `onValueComplete` | `((value: string, eventDetails: OTPFieldRootCompleteEventDetails) => void)` |  | Callback function that is fired when the OTP value becomes complete, or when a complete value is pasted while the OTP is already complete. When the value changes, it runs later than `onValueChange`, after the internal value update is applied. If a complete pasted value matches the current value, `onValueChange` does not fire. If `autoSubmit` is enabled, it runs immediately before the owning form is submitted. |
| `onValueInvalid` | `((value: string, eventDetails: OTPFieldRootInvalidEventDetails) => void)` |  | Callback fired when entered text contains characters that are rejected by validation or normalization before the OTP value updates. The `value` argument is the attempted user-entered string before normalization. |
| `readOnly` | `boolean` | `false` | Whether the user should be unable to change the field value. |
| `render` | `ReactElement<unknown, string \| JSXElementConstructor<any>> \| ComponentRenderFn<HTMLProps, OTPFieldRootState>` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a `ReactElement` or a function that returns the element to render. |
| `required` | `boolean` | `false` | Whether the user must enter a value before submitting a form. |
| `size` | `"default" \| "sm" \| "lg"` |  | The boxes' size: 28, 35 or 42 px tall. |
| `style` | `CSSProperties \| ((state: OTPFieldRootState) => CSSProperties)` |  | Style applied to the element, or a function that returns a style object based on the component's state. |
| `validationType` | `"none" \| "numeric" \| "alpha" \| "alphanumeric"` | `numeric` | The type of input validation to apply to the OTP value. |
| `value` | `string` |  | The OTP value. |
| `variant` | `"default" \| "filled"` |  | `filled` gives the boxes the grey field fill. |

### InputOTPGroup

Renders a `div` and passes it every other prop.

### InputOTPSeparator

Renders Base UI OTPField.Separator and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` |  |  |
| `orientation` | `"horizontal" \| "vertical"` | `'horizontal'` | The orientation of the separator. |
| `render` | `ReactElement<unknown, string \| JSXElementConstructor<any>> \| ComponentRenderFn<HTMLProps, SeparatorState>` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a `ReactElement` or a function that returns the element to render. |
| `style` | `CSSProperties \| ((state: SeparatorState) => CSSProperties)` |  | Style applied to the element, or a function that returns a style object based on the component's state. |

### InputOTPSlot

Renders Base UI OTPField.Input and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `index` (required) | `number` |  | The box's position, from 0. |
| `className` | `string` |  |  |
| `render` | `ReactElement<unknown, string \| JSXElementConstructor<any>> \| ComponentRenderFn<DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>, OTPFieldInputState>` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a `ReactElement` or a function that returns the element to render. |
| `style` | `CSSProperties \| ((state: OTPFieldInputState) => CSSProperties)` |  | Style applied to the element, or a function that returns a style object based on the component's state. |

Every part takes `className`, merged with its defaults by `cn()`, and `ref`, which reaches the element it renders.

**Data attributes:** `data-slot="input-otp"` (InputOTP), `data-slot="input-otp-group"` (InputOTPGroup), `data-slot="input-otp-separator"` (InputOTPSeparator), `data-slot="input-otp-slot"` (InputOTPSlot), and `data-size`, `data-variant`.

**Provider strings:** `otpCharacter` (`BooleanUIProvider`'s `strings`).

## Theming

Each box is a field: `--field` for the fill (`--field-filled` with `variant="filled"`), `--control` for the edge (`--control-hover` under the pointer), `--ring` for focus, `--invalid` for the error edge, and `--field-disabled` with `--field-disabled-foreground` when disabled. The separator is `--muted-foreground`.

| Token | Used for |
| --- | --- |
| `--control` | border |
| `--control-hover` | border |
| `--field` | background |
| `--field-disabled` | background |
| `--field-disabled-foreground` | text |
| `--field-filled` | background |
| `--foreground` | text |
| `--invalid` | border |
| `--muted-foreground` | text |
| `--primary` | background |
| `--primary-foreground` | text |
| `--ring` | border |
