# OTP Field (/ui/components/react/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 customize 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` grays 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).


## 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              | Type                   | Default           | Description                        |
| ----------------- | ------------------------------------------------------------- | ----------------- | ------------------------------------------------------------------------- |
| `length`          | `number`                                                      | -                 | **Required.** Number of input slots.                                      |
| `variant`         | `'outline' \| 'soft'`                              | `'outline'`       | Slot appearance - bordered or filled.                                     |
| `size`            | `'sm' \| 'md' \| 'lg'`                             | `'md'`            | Scales every slot.                                                        |
| `value`           | `string`                                                      | -                 | Controlled value. Pair with `onValueChange`.                              |
| `defaultValue`    | `string`                                                      | -                 | Uncontrolled initial value.                                               |
| `onValueChange`   | `(value: string, details) => void`                 | -                 | Fires whenever the value changes (typing, paste, clear).                  |
| `onValueComplete` | `(value: string, details) => void`                 | -                 | Fires once every slot is filled.                                          |
| `validationType`  | `'numeric' \| 'alpha' \| 'alphanumeric' \| 'none'` | `'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.                                                      |
| `inputMode`       | `string`                                                      | -                 | Virtual-keyboard hint for the slot inputs.                                |
| `readOnly`        | `boolean`                                                     | `false`           | Prevent the user changing the value.                                      |
| `normalizeValue`  | `function`                                                    | -                 | Normalize the value after filtering, whenever the field normalizes input. |
| `onValueInvalid`  | `function`                                                    | -                 | Fires when typed text is rejected by validation or normalization.         |
| `render`          | `ReactElement \| ((props, state) => ReactElement)` | -                 | Replace the root element, or compose it with another component.           |
| `className`       | `string \| ((state) => string)`                    | -                 | 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                                                           |
| ----------- | ------------------------------------------------------------- | ------- | --------------------------------------------------------------------- |
| `render`    | `ReactElement \| ((props, state) => ReactElement)` | -       | Replace the underlying element, or compose it with another component. |
| `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                                                           |
| ------------- | ------------------------------------------------------------- | -------------- | --------------------------------------------------------------------- |
| `render`      | `ReactElement \| ((props, state) => ReactElement)` | -              | Replace the underlying element, or compose it with another component. |
| `orientation` | `'horizontal' \| 'vertical'`                       | `'horizontal'` | Layout axis.                                                          |
| `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.
