# OTP Field (/ui/components/react/otp-field)



<Playground component="otp-field" />

## Usage [#usage]

```tsx
import { OTPField, OTPFieldInput } from '@appica/ui-react/otp-field'
```

```tsx
<OTPField length={6} aria-label="Verification code">
  {Array.from({ length: 6 }, (_, i) => (
    <OTPFieldInput key={i} aria-label={`Digit ${i + 1} of 6`} />
  ))}
</OTPField>
```

`OTPField` is a compound component for one-time codes. The root owns the value and `length`; you render one [`OTPFieldInput`](#otpfieldinput) per slot (commonly mapped over `length`). Typing fills slots left to right and auto-advances, **Backspace** moves back, pasting a full code distributes across the slots, and `onValueComplete` fires when every slot is filled.

Give the group an accessible name with `aria-label` (or `aria-labelledby`) and label each slot — the inputs aren't tied to a single `<label>` the way a plain field is.

## Examples [#examples]

### Variants [#variants]

`variant` on the root switches every slot between a bordered `outline` (default) and a filled `soft` field.

```tsx
import { OTPField, OTPFieldInput } from '@appica/ui-react/otp-field'

const LENGTH = 6

export default function OTPFieldVariants() {
  return (
    <div className="flex flex-col gap-5">
      <OTPField length={LENGTH} variant="outline" aria-label="Outline code">
        {Array.from({ length: LENGTH }, (_, i) => (
          <OTPFieldInput key={i} aria-label={`Digit ${i + 1} of ${LENGTH}`} />
        ))}
      </OTPField>
      <OTPField length={LENGTH} variant="soft" aria-label="Soft code">
        {Array.from({ length: LENGTH }, (_, i) => (
          <OTPFieldInput key={i} aria-label={`Digit ${i + 1} of ${LENGTH}`} />
        ))}
      </OTPField>
    </div>
  )
}
```

### Sizes [#sizes]

`size` on the root scales all slots together — `sm`, `md` (default), or `lg`.

```tsx
import { OTPField, OTPFieldInput } from '@appica/ui-react/otp-field'

const LENGTH = 4

function Field({ size, label }: { size: 'sm' | 'md' | 'lg'; label: string }) {
  return (
    <OTPField length={LENGTH} size={size} aria-label={label}>
      {Array.from({ length: LENGTH }, (_, i) => (
        <OTPFieldInput key={i} aria-label={`Digit ${i + 1} of ${LENGTH}`} />
      ))}
    </OTPField>
  )
}

export default function OTPFieldSizes() {
  return (
    <div className="flex flex-col items-center gap-5">
      <Field size="sm" label="Small code" />
      <Field size="md" label="Medium code" />
      <Field size="lg" label="Large code" />
    </div>
  )
}
```

### With a separator [#with-a-separator]

Drop an [`OTPFieldSeparator`](#otpfieldseparator) between slots to break a long code into groups. It renders a dash by default; pass children to customise it.

```tsx
import { OTPField, OTPFieldInput, OTPFieldSeparator } from '@appica/ui-react/otp-field'

export default function OTPFieldWithSeparator() {
  return (
    <OTPField length={6} aria-label="Verification code">
      <OTPFieldInput aria-label="Digit 1 of 6" />
      <OTPFieldInput aria-label="Digit 2 of 6" />
      <OTPFieldInput aria-label="Digit 3 of 6" />
      <OTPFieldSeparator />
      <OTPFieldInput aria-label="Digit 4 of 6" />
      <OTPFieldInput aria-label="Digit 5 of 6" />
      <OTPFieldInput aria-label="Digit 6 of 6" />
    </OTPField>
  )
}
```

### Placeholder [#placeholder]

Give each `OTPFieldInput` a `placeholder` to hint at empty slots — a single `•` is a common choice. It shows only while a slot is empty and unfocused.

```tsx
import { OTPField, OTPFieldInput } from '@appica/ui-react/otp-field'

const LENGTH = 6

export default function OTPFieldPlaceholder() {
  return (
    <OTPField length={LENGTH} aria-label="Verification code">
      {Array.from({ length: LENGTH }, (_, i) => (
        <OTPFieldInput key={i} placeholder="•" aria-label={`Digit ${i + 1} of ${LENGTH}`} />
      ))}
    </OTPField>
  )
}
```

### Masked [#masked]

Set `mask` to obscure the entered characters — useful when the code is sensitive and shouldn't be shown on screen.

```tsx
import { OTPField, OTPFieldInput } from '@appica/ui-react/otp-field'

const LENGTH = 6

export default function OTPFieldMask() {
  return (
    <OTPField length={LENGTH} mask defaultValue="135790" aria-label="Masked code">
      {Array.from({ length: LENGTH }, (_, i) => (
        <OTPFieldInput key={i} aria-label={`Digit ${i + 1} of ${LENGTH}`} />
      ))}
    </OTPField>
  )
}
```

### Controlled & verify on complete [#controlled--verify-on-complete]

Drive the value with `value` + `onValueChange`, and use `onValueComplete` to act once all slots are filled — typically to verify or submit the code.

```tsx
'use client'

import { useState } from 'react'
import { OTPField, OTPFieldInput } from '@appica/ui-react/otp-field'

const LENGTH = 6
const CODE = '123456'

export default function OTPFieldControlled() {
  const [value, setValue] = useState('')
  const [status, setStatus] = useState<'idle' | 'valid' | 'invalid'>('idle')

  return (
    <div className="flex flex-col items-center gap-3">
      <OTPField
        length={LENGTH}
        value={value}
        onValueChange={(next) => {
          setValue(next)
          setStatus('idle')
        }}
        onValueComplete={(next) => setStatus(next === CODE ? 'valid' : 'invalid')}
        aria-label="Verification code"
      >
        {Array.from({ length: LENGTH }, (_, i) => (
          <OTPFieldInput key={i} aria-label={`Digit ${i + 1} of ${LENGTH}`} />
        ))}
      </OTPField>
      <p className="text-foreground-muted text-sm" aria-live="polite">
        {status === 'valid'
          ? '✓ Code verified'
          : status === 'invalid'
            ? `✗ Incorrect code (try ${CODE})`
            : `Enter the 6-digit code (${CODE})`}
      </p>
    </div>
  )
}
```

### Disabled, read-only & error states [#disabled-read-only--error-states]

`disabled` greys out every slot and blocks interaction; `readOnly` keeps the code visible and focusable but not editable. For the error state, place the field in a [`Field`](/ui/components/react/field) marked `invalid` — `data-invalid` propagates to every slot (OTP derives its invalid state from Field context, not a standalone `aria-invalid`).

```tsx
import { Field } from '@appica/ui-react/field'
import { OTPField, OTPFieldInput } from '@appica/ui-react/otp-field'

const LENGTH = 6

const slots = (prefix: string) =>
  Array.from({ length: LENGTH }, (_, i) => (
    <OTPFieldInput key={i} aria-label={`${prefix} digit ${i + 1} of ${LENGTH}`} />
  ))

export default function OTPFieldStates() {
  return (
    <div className="flex flex-col gap-5">
      <OTPField length={LENGTH} disabled aria-label="Disabled code">
        {slots('Disabled')}
      </OTPField>
      <OTPField length={LENGTH} readOnly defaultValue="123456" aria-label="Read-only code">
        {slots('Read-only')}
      </OTPField>
      <Field invalid>
        <OTPField length={LENGTH} defaultValue="123456" aria-label="Code with error">
          {slots('Error')}
        </OTPField>
      </Field>
    </div>
  )
}
```

## RTL [#rtl]

Every Appica UI component supports right-to-left layouts out of the box. Set the `dir` attribute on a container (commonly your `<html>` element) so CSS logical properties resolve correctly, and wrap your tree in `DirectionProvider` so direction-aware behavior — roving focus, popup placement, and the like — follows the same direction.

```tsx
import { DirectionProvider } from '@appica/ui-react/providers/direction-provider'

export default function RootLayout({ children }) {
  return (
    <html lang="ar" dir="rtl">
      <body>
        <DirectionProvider dir="rtl">{children}</DirectionProvider>
      </body>
    </html>
  )
}
```

The slots lay out from the right and focus advances start-to-end following the resolved direction. For setup details and caveats, see the [RTL guide](/ui/docs/react/rtl).

<RtlPreview component="otp-field" />

## API reference [#api-reference]

`OTPField` wraps [Base UI's OTP Field](https://base-ui.com/react/components/otp-field). Each part forwards its remaining props to the matching Base UI primitive — the tables below list the Appica additions and the most common props.

### OTPField [#otpfield]

The root. Owns the value and the shared `variant` / `size` (cascaded to every slot); renders a `<div>`.

| Prop              | <ColMinWidth width="200">Type</ColMinWidth>                   | Default           | <ColMinWidth width="220">Description</ColMinWidth>              |
| ----------------- | ------------------------------------------------------------- | ----------------- | --------------------------------------------------------------- |
| `length`          | `number`                                                      | —                 | **Required.** Number of input slots.                            |
| `variant`         | <Code>'outline' \| 'soft'</Code>                              | `'outline'`       | Slot appearance — bordered or filled.                           |
| `size`            | <Code>'sm' \| 'md' \| 'lg'</Code>                             | `'md'`            | Scales every slot.                                              |
| `value`           | `string`                                                      | —                 | Controlled value. Pair with `onValueChange`.                    |
| `defaultValue`    | `string`                                                      | —                 | Uncontrolled initial value.                                     |
| `onValueChange`   | <Code>(value: string, details) => void</Code>                 | —                 | Fires whenever the value changes (typing, paste, clear).        |
| `onValueComplete` | <Code>(value: string, details) => void</Code>                 | —                 | Fires once every slot is filled.                                |
| `validationType`  | <Code>'numeric' \| 'alpha' \| 'alphanumeric' \| 'none'</Code> | `'numeric'`       | Which characters the slots accept.                              |
| `mask`            | `boolean`                                                     | `false`           | Obscure the entered characters.                                 |
| `autoSubmit`      | `boolean`                                                     | `false`           | Submit the owning form when the code completes.                 |
| `autoComplete`    | `string`                                                      | `'one-time-code'` | Applied to the first slot and hidden input for OS autofill.     |
| `name`            | `string`                                                      | —                 | Field name submitted with a form.                               |
| `required`        | `boolean`                                                     | `false`           | A complete value is required before the form submits.           |
| `disabled`        | `boolean`                                                     | `false`           | Disables every slot.                                            |
| `render`          | <Code>ReactElement \| ((props, state) => ReactElement)</Code> | —                 | Replace the root element, or compose it with another component. |
| `className`       | <Code>string \| ((state) => string)</Code>                    | —                 | Extra classes on the root, merged via `tailwind-merge`.         |

### OTPFieldInput [#otpfieldinput]

One slot. Inherits `variant` / `size` from the root via context; renders an `<input>`.

| Prop        | Type     | Default | Description                                             |
| ----------- | -------- | ------- | ------------------------------------------------------- |
| `className` | `string` | —       | Extra classes on the slot, merged via `tailwind-merge`. |

Also forwards every native `<input>` prop (`aria-label`, `ref`, …). It exposes `data-*` state attributes (`data-disabled`, `data-complete`, `data-filled`, …) for styling.

### OTPFieldSeparator [#otpfieldseparator]

A divider between slot groups. Renders a dash by default.

| Prop        | Type        | Default | Description                                        |
| ----------- | ----------- | ------- | -------------------------------------------------- |
| `children`  | `ReactNode` | dash    | Replace the default dash mark with custom content. |
| `className` | `string`    | —       | Extra classes, merged via `tailwind-merge`.        |

## Accessibility [#accessibility]

* Each slot is a native `<input>` (`role="textbox"`). Give every slot an `aria-label` (e.g. `Digit 1 of 6`) and name the whole group with `aria-label` or `aria-labelledby`.
* Typing fills slots left to right and auto-advances; **Backspace*&#x2A; clears and moves back; &#x2A;*←/→** move between slots; pasting a full code distributes it across the slots.
* `autoComplete="one-time-code"` lets the OS offer codes from SMS for autofill.
* `mask` obscures the characters for sensitive codes.
* `disabled` removes the slots from the tab order and exposes `data-disabled` for styling.
