# OtpInput

> One-time-code entry with N boxes; paste fills every box, Backspace steps back, digits stay left-to-right in RTL.

Source: https://docs.nasaqui.com/components/otp-input

## Install

```bash
npx shadcn@latest add https://docs.nasaqui.com/r/otp-input.json
```

A row of single-character boxes for verification codes and PINs. Typing advances, Backspace steps back, paste
(or an SMS autofill) fills all boxes at once. The group is always left-to-right so a code reads the same in
Arabic pages.

## When to use

- SMS, email or authenticator codes; short PINs.

## When not to use

- Long or free-form tokens: use [`Input`](https://docs.nasaqui.com/components/field).

## Import

```tsx
import { OtpInput } from "@fadymondy/nasaq/web";
// inside this monorepo: "@nasaq/web"
```

## Quick start

```tsx
import { Field, FieldLabel, OtpInput } from "@fadymondy/nasaq/web";

export function VerifyField() {
  return (
    <Field className="w-fit">
      <FieldLabel>Verification code</FieldLabel>
      <OtpInput name="code" onComplete={(code) => console.log(code)} />
    </Field>
  );
}
```

## Anatomy

```
OtpInput            data-slot="otp-input" (role="group", dir="ltr")
├─ box × length     data-slot="otp-input-box" (Base UI Input; data-filled when it has a character)
└─ hidden input     only when `name` is set; carries the joined code
```

## API

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `length` | `number` | `6` | Number of boxes. |
| `value` | `string` | none | Controlled code. |
| `defaultValue` | `string` | `""` | Initial code when uncontrolled. |
| `onValueChange` | `(value: string) => void` | none | Fires on every change with the joined code. |
| `onComplete` | `(value: string) => void` | none | Fires when all boxes are filled. |
| `type` | `"numeric" \| "alphanumeric"` | `"numeric"` | Accepted characters; numeric opens the digit keypad. |
| `name` | `string` | none | Form name; a hidden input submits the code. |
| `disabled` | `boolean` | `false` | Disable every box. |
| `invalid` | `boolean` | `false` | Danger border and `aria-invalid`. |
| `autoFocus` | `boolean` | `false` | Focus the first box on mount. |
| `getBoxLabel` | `(index: number, length: number) => string` | `"Digit i of n"` | Accessible name per box. Localise. |

Other props go to the wrapping `div`.

## Examples

Four-digit PIN, controlled, Arabic labels:

```tsx
import { Field, FieldLabel, OtpInput } from "@fadymondy/nasaq/web";
import { useState } from "react";

export function Pin() {
  const [pin, setPin] = useState("");
  return (
    <Field className="w-fit">
      <FieldLabel>الرمز السري</FieldLabel>
      <OtpInput
        length={4}
        value={pin}
        onValueChange={setPin}
        getBoxLabel={(i, n) => `الخانة ${i + 1} من ${n}`}
      />
    </Field>
  );
}
```

## Accessibility

| Key | Action |
| --- | --- |
| Type | Fills the box and moves to the next. |
| Backspace | Clears the box; on an empty box, moves back and clears the previous one. |
| Left / Right | Previous / next box (physical, since the group is LTR). |
| Home / End | First / last box. |
| Paste | Fills all boxes from the clipboard, ignoring disallowed characters. |

Boxes use `autocomplete="one-time-code"` so mobile keyboards offer the SMS code. Each box has an accessible
name from `getBoxLabel`; the group is `role="group"`.

## RTL & i18n

`dir="ltr"` is set on the group, so the first digit is on the left even on RTL pages. The surrounding label and
messages still follow the page direction. Localise `getBoxLabel`.

## Styling & tokens

Uses `border-input`, `bg-card`, `border-nq-focus`, `border-nq-danger`, `size-control`, `rounded-control`.
State attributes: `data-filled`, `data-invalid`. Extend with `className` (on the group).

## Do / Don't

- Do show where the code was sent in the field description.
- Don't use it for values longer than about 8 characters.

## Related

- [Field](https://docs.nasaqui.com/components/field)
- [InputGroup](https://docs.nasaqui.com/components/input-group)

## Lab

`https://docs.nasaqui.com/?path=/docs/components-forms-otpinput--docs`

## Code

### React

```tsx
import { Field, FieldLabel, OtpInput } from "@fadymondy/nasaq/web";

export function VerifyField() {
  return (
    <Field className="w-fit">
      <FieldLabel>Verification code</FieldLabel>
      <OtpInput name="code" onComplete={(code) => console.log(code)} />
    </Field>
  );
}
```

### shadcn

```tsx
import { Field, FieldLabel } from "@/components/ui/field";
import { OtpInput } from "@/components/ui/otp-input";

export function VerifyField() {
  return (
    <Field className="w-fit">
      <FieldLabel>Verification code</FieldLabel>
      <OtpInput name="code" onComplete={(code) => console.log(code)} />
    </Field>
  );
}
```

### Vue

```vue
<script setup lang="ts">
import { NqOtpInput } from "@fadymondy/nasaq/vue";
</script>

<template>
  <div class="flex w-fit flex-col gap-1.5">
    <span class="text-label text-foreground">Verification code</span>
    <NqOtpInput name="code" @complete="(code) => console.log(code)" />
  </div>
</template>
```

### Blade

```blade
<div class="flex w-fit flex-col gap-1.5">
    <span class="text-label text-foreground">Verification code</span>
    <x-nq::otp-input name="code" />
</div>
```

### HTML + Alpine

```html
<div class="flex w-fit flex-col gap-1.5">
    <span class="text-label text-foreground">Verification code</span>
    <div role="group" dir="ltr" data-slot="otp-input" x-data="nqOtpInput('', 6, 'numeric')" x-modelable="value"
    class="inline-flex items-center gap-2">
            <input data-slot="otp-input-box" x-bind="box(0)" type="text" value=""
                        aria-label="Digit 1 of 6"
            inputmode="numeric" autocomplete="one-time-code" autocapitalize="off" spellcheck="false"
                          class="size-control min-h-[var(--nq-touch-min,0px)] min-w-0 rounded-control border border-input bg-card p-0 text-center text-body font-medium tabular-nums text-foreground transition-colors duration-150 ease-nq outline-none focus-visible:border-nq-focus focus-visible:outline-1 focus-visible:outline-nq-focus data-invalid:border-nq-danger aria-invalid:border-nq-danger disabled:cursor-not-allowed disabled:opacity-50 pointer-coarse:text-[16px]" />
            <input data-slot="otp-input-box" x-bind="box(1)" type="text" value=""
                        aria-label="Digit 2 of 6"
            inputmode="numeric" autocomplete="one-time-code" autocapitalize="off" spellcheck="false"
                          class="size-control min-h-[var(--nq-touch-min,0px)] min-w-0 rounded-control border border-input bg-card p-0 text-center text-body font-medium tabular-nums text-foreground transition-colors duration-150 ease-nq outline-none focus-visible:border-nq-focus focus-visible:outline-1 focus-visible:outline-nq-focus data-invalid:border-nq-danger aria-invalid:border-nq-danger disabled:cursor-not-allowed disabled:opacity-50 pointer-coarse:text-[16px]" />
            <input data-slot="otp-input-box" x-bind="box(2)" type="text" value=""
                        aria-label="Digit 3 of 6"
            inputmode="numeric" autocomplete="one-time-code" autocapitalize="off" spellcheck="false"
                          class="size-control min-h-[var(--nq-touch-min,0px)] min-w-0 rounded-control border border-input bg-card p-0 text-center text-body font-medium tabular-nums text-foreground transition-colors duration-150 ease-nq outline-none focus-visible:border-nq-focus focus-visible:outline-1 focus-visible:outline-nq-focus data-invalid:border-nq-danger aria-invalid:border-nq-danger disabled:cursor-not-allowed disabled:opacity-50 pointer-coarse:text-[16px]" />
            <input data-slot="otp-input-box" x-bind="box(3)" type="text" value=""
                        aria-label="Digit 4 of 6"
            inputmode="numeric" autocomplete="one-time-code" autocapitalize="off" spellcheck="false"
                          class="size-control min-h-[var(--nq-touch-min,0px)] min-w-0 rounded-control border border-input bg-card p-0 text-center text-body font-medium tabular-nums text-foreground transition-colors duration-150 ease-nq outline-none focus-visible:border-nq-focus focus-visible:outline-1 focus-visible:outline-nq-focus data-invalid:border-nq-danger aria-invalid:border-nq-danger disabled:cursor-not-allowed disabled:opacity-50 pointer-coarse:text-[16px]" />
            <input data-slot="otp-input-box" x-bind="box(4)" type="text" value=""
                        aria-label="Digit 5 of 6"
            inputmode="numeric" autocomplete="one-time-code" autocapitalize="off" spellcheck="false"
                          class="size-control min-h-[var(--nq-touch-min,0px)] min-w-0 rounded-control border border-input bg-card p-0 text-center text-body font-medium tabular-nums text-foreground transition-colors duration-150 ease-nq outline-none focus-visible:border-nq-focus focus-visible:outline-1 focus-visible:outline-nq-focus data-invalid:border-nq-danger aria-invalid:border-nq-danger disabled:cursor-not-allowed disabled:opacity-50 pointer-coarse:text-[16px]" />
            <input data-slot="otp-input-box" x-bind="box(5)" type="text" value=""
                        aria-label="Digit 6 of 6"
            inputmode="numeric" autocomplete="one-time-code" autocapitalize="off" spellcheck="false"
                          class="size-control min-h-[var(--nq-touch-min,0px)] min-w-0 rounded-control border border-input bg-card p-0 text-center text-body font-medium tabular-nums text-foreground transition-colors duration-150 ease-nq outline-none focus-visible:border-nq-focus focus-visible:outline-1 focus-visible:outline-nq-focus data-invalid:border-nq-danger aria-invalid:border-nq-danger disabled:cursor-not-allowed disabled:opacity-50 pointer-coarse:text-[16px]" />
        <input type="hidden" name="code" :value="value" value="" /></div>
</div>
```
