<!-- Protocore Design System — OTPInput -->
# OTPInput

- **Category:** Inputs (`inputs`)
- **Slug:** `inputs/otp-input`
- **Status:** stable
- **Platforms:** web
- **Import:** `import { OTPInput } from "@protocore/pds";`
- **Docs:** https://pds.protocore.io/components/inputs/otp-input

> A row of single-character cells for one-time-code / verification entry, with auto-advance, paste distribution, and arrow navigation.

## When to use it

Use **OTPInput** for short, fixed-length codes a user transcribes from a text message, authenticator app, or email — 2FA codes, email confirmations, recovery keys. The segmented cells make length obvious and paste-friendly. For free-form numeric entry (quantities, amounts) use **NumberInput**; for ordinary text use **Input**.

## Props

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `aria-label` | `string` | no | `Verification code` | Accessible label for the cell group. Default `"Verification code"`. |
| `className` | `string` | no | — | — |
| `defaultValue` | `string` | no | — | Initial value when uncontrolled. |
| `disabled` | `boolean` | no | `false` | Disable every cell. |
| `length` | `number` | no | `6` | Number of character cells. Default `6`. |
| `mask` | `boolean` | no | `false` | Obscure entered characters (renders each cell as a password box). |
| `onComplete` | `((value: string) => void)` | no | — | Fires once with the full string when every cell is filled. |
| `onValueChange` | `((value: string) => void)` | no | — | Fires with the full concatenated string whenever any cell changes. |
| `style` | `CSSProperties` | no | — | — |
| `type` | `enum` | no | `numeric` | Accepted character class: `"numeric"` (0–9, default) or `"alphanumeric"`. |
| `value` | `string` | no | — | Controlled value. Pair with `onValueChange`. |

## Examples

### Basics

Six numeric cells. Typing auto-advances, `onValueChange` reports the concatenated string on every edit, and `onComplete` fires once when the last cell fills. Controllable via `value` / `defaultValue`.

```tsx
import { useState } from "react";
import { OTPInput, Text, Badge } from "@protocore/pds";

export default function Demo() {
  const [code, setCode] = useState("");
  const [done, setDone] = useState(false);

  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 12, alignItems: "flex-start" }}>
      <OTPInput
        length={6}
        value={code}
        onValueChange={(v) => {
          setCode(v);
          setDone(false);
        }}
        onComplete={() => setDone(true)}
        aria-label="Two-factor code"
      />
      {done ? (
        <Badge tone="success">Code complete — verifying</Badge>
      ) : (
        <Text as="span" size="sm" mono color="muted">
          Entered: {code || "—"}
        </Text>
      )}
    </div>
  );
}
```

### Alphanumeric & masked

Set `type="alphanumeric"` to accept letters and digits, and `mask` to obscure each character — the pattern for backup / recovery keys. `length` sizes the row.

```tsx
import { useState } from "react";
import { OTPInput, Text } from "@protocore/pds";

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

  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 12, alignItems: "flex-start" }}>
      <Text as="span" size="sm" color="secondary">
        Enter your 8-character recovery key
      </Text>
      <OTPInput
        length={8}
        type="alphanumeric"
        mask
        value={value}
        onValueChange={setValue}
        aria-label="Recovery key"
      />
    </div>
  );
}
```

## Do & don't

**Do**

- Give the group an `aria-label` describing the code (e.g. "Two-factor code").
- Match `length` to the real code length so paste fills exactly.
- Use `type="numeric"` for digit-only codes — it sets the numeric `inputMode` for mobile keypads.
- Handle `onComplete` to auto-submit, but still allow editing before the request resolves.

**Don't**

- Don't use OTPInput for variable-length secrets like passwords — use a masked Input.
- Don't stack more cells than the code has; empty trailing cells can never complete.
- Don't disable paste — users routinely paste codes from another app.
- Don't rely on cell color alone to signal an error; pair with a Field error message.

## Accessibility

**Keyboard**

| Keys | Action |
| --- | --- |
| `0–9 / A–Z` | Fill the focused cell and advance to the next. |
| `Backspace` | Clear the focused cell, or step back and clear the previous one if empty. |
| `Delete` | Clear the focused cell in place. |
| `Arrow Left / Right` | Move between cells without changing values. |
| `Home / End` | Jump to the first / last cell. |
| `Paste` | Distribute the pasted characters across cells from the focused one. |

**Notes**

- The cells are wrapped in a `role="group"` labelled by `aria-label`; each cell carries a positional label ("Digit 1 of 6").
- Focusing a cell selects its content so a keystroke replaces the character.
- The first cell advertises `autoComplete="one-time-code"` so iOS / Android can offer the SMS code.

## Related

`input`, `field`, `number-input`

---

© Protocore. All rights reserved. Use of the Protocore Design System requires prior written authorization from Protocore (contact@protocore.io). These machine-readable materials must not be ingested into ML-training datasets or derivative design systems. See https://pds.protocore.io/legal/
