## OTP Field (S—001)

A six-slot verification code field with paste, autofill, and verifying, error and success states.

- URL: https://sundaeui.dev/components/otp-field
- Category: Forms & inputs · Art direction: Refined
- Access: Free (no account needed)
- Stack: React 18+, TypeScript, plain CSS; dependencies: none
- Install: `npx shadcn@latest add https://sundaeui.dev/r/otp-field.json`
- Tags: form, react, input, otp, verification, authentication

Every slot is drawn individually, but there is only one native input underneath. That single input is why paste, SMS autofill on iOS and Android, undo, selection and screen readers all behave as people expect. Nothing is re-implemented.
Digits settle into their slots with a short blur-in. The active slot lifts with an accent ring and a blinking caret. Verifying sends a soft wave across the row, an error shakes once and holds a red edge, and success turns the slots green one after another.
Use it for email or SMS verification, 2FA, and confirming sensitive actions. Sundae’s own sign-up flow uses this component.

### Usage

```tsx
import { OtpField, type OtpStatus } from '@/components/sundae/otp-field'

export function VerifyEmail() {
  const [status, setStatus] = useState<OtpStatus>('idle')

  return (
    <OtpField
      autoFocus
      status={status}
      onComplete={async (code) => {
        setStatus('verifying')
        const ok = await verifyCode(code)
        setStatus(ok ? 'success' : 'error')
      }}
    />
  )
}
```

### Props

- `length` (`number`, default `6`): Number of slots.
- `value` (`string`): Controlled value. Omit for uncontrolled use.
- `onChange` (`(value: string) => void`): Called with the sanitised value on every change.
- `onComplete` (`(code: string) => void | Promise<unknown>`): Called once each time every slot is filled with a new code.
- `status` (`'idle' | 'verifying' | 'error' | 'success' | 'expired'`, default `'idle'`): Visual and ARIA state. While verifying or after success, the field is read-only.
- `label` (`string`, default `'Verification code'`): Accessible name of the input.
- `groupSize` (`number`, default `3`): Draws a separator after this many slots. Pass 0 to disable.
- `pattern` (`RegExp`, default `/^[0-9]$/`): Single-character allow-list. Non-digit patterns switch the mobile keyboard to text.
- `describedBy` (`string`): id of helper or error text, wired to aria-describedby.
- `autoFocus` (`boolean`): Focus on mount.
- `disabled` (`boolean`): Disables the field.
- `name` (`string`, default `'code'`): Form field name, so the field works inside a native <form>.

### Accessibility

- Built on a single native <input> with autocomplete="one-time-code", so assistive technology reads it as one text field, not six.
- Sets aria-invalid in the error and expired states and aria-busy while verifying.
- After an error, the whole code is selected so the next keystroke starts over.
- With prefers-reduced-motion, every animation finishes instantly and the caret stops blinking.
