# Input OTP

An accessible React one-time code input for Next.js and Tailwind: boxes that move on as you type and take a pasted code, built on Radix.

Source: https://ui.fewtone.com/components/input-otp

## Installation

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

## Usage

```tsx
import {
  InputOTP,
  InputOTPGroup,
  InputOTPSeparator,
  InputOTPSlot,
} from "@/components/ui/input-otp"

export function VerificationCode() {
  return (
    <InputOTP aria-label="Verification code" name="code">
      <InputOTPGroup>
        <InputOTPSlot index={0} />
        <InputOTPSlot index={1} />
        <InputOTPSlot index={2} />
      </InputOTPGroup>
      <InputOTPSeparator />
      <InputOTPGroup>
        <InputOTPSlot index={3} />
        <InputOTPSlot index={4} />
        <InputOTPSlot index={5} />
      </InputOTPGroup>
    </InputOTP>
  )
}
```

## API reference

### InputOTP

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `string` | – | The code, when you control it. |
| `defaultValue` | `string` | – | The code at first. |
| `onValueChange` | `(value: string) => void` | – | Called with the whole code after every change. |
| `size` | `"sm" \| "md" \| "lg"` | `"md"` | 32, 36 or 40px boxes, matching Input's heights. |
| `validationType` | `"numeric" \| "alpha" \| "alphanumeric" \| "none"` | `"numeric"` | Which characters a box accepts; others are ignored. Use sanitizeValue with none for your own rule. |
| `type` | `"text" \| "password"` | `"text"` | password hides each character behind a dot. |
| `autoSubmit` | `boolean` | `false` | Submits the form once every box is filled. onAutoSubmit is called first. |
| `autoComplete` | `"one-time-code" \| "off"` | `"one-time-code"` | Lets the phone or a password manager fill in a code it received. |
| `name` | `string` | – | Submits the code with a form, through a hidden input. |
| `status` | `"success" \| "error"` | – | The result of checking the code. success runs a wave along the boxes and makes their borders solid; error shakes the field and marks it invalid. Leave it unset while the code is typed or checked. |
| `aria-invalid` | `boolean` | – | Marks the code as wrong without the shake: the boxes and focus box turn destructive. |
| `disabled` | `boolean` | `false` | Prevents typing. |
| `placeholder` | `string` | – | One character per box, shown while the code is empty. |

### InputOTPGroup

| Prop | Type | Default | Description |
| --- | --- | --- | --- |

### InputOTPSlot

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `index` | `number` | – | The box's place in the code, from 0. |

### InputOTPSeparator

| Prop | Type | Default | Description |
| --- | --- | --- | --- |

**When to use it:** Use InputOTP for one-time codes, verification codes and short PINs, where each character gets its own box. Use Input for passwords and other free text.
**Built on:** Radix One-Time Password Field.
**npm packages (installed by the CLI):** lucide-react@^1.52.0, motion@^14.0.0, radix-ui@^1.7.0.

## Examples

### Six digits

Type and the focus box glides on to the next box as each digit rises into place; paste a whole code and it fills in left to right. Once it's in, it's checked: 123456 runs a wave along the boxes, anything else shakes the field. Set status from your own check.

```tsx
"use client";

import {
  InputOTP,
  InputOTPGroup,
  InputOTPSeparator,
  InputOTPSlot,
  type InputOTPStatus,
} from "@/components/ui/input-otp";
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select";
import { Spinner } from "@/components/ui/spinner";
import { duration, ease, swap } from "@/lib/motion";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { useEffect, useId, useState } from "react";

type Size = "sm" | "md" | "lg";

/** How long the pretend server takes to check a code, in ms */
const CHECK = 600;
const RIGHT = "123456";

/*
 * A six-digit code in two groups of three, with the size in the top-right corner. Once all six digits are
 * in, the code is checked for a moment, then the boxes run their success wave (123456) or the field shakes
 * its head (anything else), so the whole range shows here. The message under it swaps through the blur in
 * a polite live region; change a digit and it waits again. An empty row at the bottom, as tall as the
 * setting's row, keeps the code centred.
 */
export default function InputOTPCode() {
  const id = useId();
  const reduce = useReducedMotion();
  const [size, setSize] = useState<Size>("md");
  const [code, setCode] = useState("");
  const [result, setResult] = useState<InputOTPStatus | "checking" | null>(null);

  useEffect(() => {
    if (code.length < RIGHT.length) {
      setResult(null);
      return;
    }
    setResult("checking");
    const timer = setTimeout(() => setResult(code === RIGHT ? "success" : "error"), CHECK);
    return () => clearTimeout(timer);
  }, [code]);

  const message =
    result === "checking" ? (
      <span className="flex items-center gap-2 text-muted-foreground">
        <Spinner size="sm" aria-hidden />
        Checking the code…
      </span>
    ) : result === "success" ? (
      <span className="flex items-center gap-2 text-foreground">
        <svg viewBox="0 0 24 24" fill="none" aria-hidden className="size-(--size-icon-sm)">
          <motion.path
            d="M5 12.5l4.5 4.5L19 7.5"
            stroke="currentColor"
            strokeWidth="2.5"
            strokeLinecap="round"
            strokeLinejoin="round"
            initial={{ pathLength: reduce ? 1 : 0 }}
            animate={{ pathLength: 1 }}
            transition={{ duration: duration.slow, ease: ease.out, delay: reduce ? 0 : duration.instant }}
          />
        </svg>
        Code verified
      </span>
    ) : result === "error" ? (
      <span className="text-destructive">That code isn’t right. Try {RIGHT}.</span>
    ) : (
      <span className="text-muted-foreground">
        Type or paste the 6 digits. {RIGHT} works; anything else fails.
      </span>
    );

  return (
    <div className="flex w-full flex-col gap-6 self-stretch">
      <div className="-mt-6 -mr-6 flex items-center justify-end gap-2">
        <label htmlFor={`${id}-size`} className="text-sm text-muted-foreground">
          Size
        </label>
        <Select value={size} onValueChange={(value) => setSize(value as Size)}>
          <SelectTrigger id={`${id}-size`} size="sm" className="w-28">
            <SelectValue />
          </SelectTrigger>
          <SelectContent>
            <SelectItem value="sm">Small</SelectItem>
            <SelectItem value="md">Medium</SelectItem>
            <SelectItem value="lg">Large</SelectItem>
          </SelectContent>
        </Select>
      </div>

      <div className="flex flex-1 flex-col items-center justify-center gap-3">
        <span id={`${id}-label`} className="text-sm font-medium">
          Verification code
        </span>
        <InputOTP
          size={size}
          value={code}
          onValueChange={setCode}
          status={result === "success" || result === "error" ? result : undefined}
          aria-labelledby={`${id}-label`}
          aria-describedby={`${id}-message`}
        >
          <InputOTPGroup>
            <InputOTPSlot index={0} />
            <InputOTPSlot index={1} />
            <InputOTPSlot index={2} />
          </InputOTPGroup>
          <InputOTPSeparator />
          <InputOTPGroup>
            <InputOTPSlot index={3} />
            <InputOTPSlot index={4} />
            <InputOTPSlot index={5} />
          </InputOTPGroup>
        </InputOTP>
        <div id={`${id}-message`} aria-live="polite" className="grid text-sm">
          <AnimatePresence initial={false} mode="popLayout">
            <motion.p
              key={result ?? "waiting"}
              initial={reduce ? { opacity: 0 } : swap.initial}
              animate={reduce ? { opacity: 1 } : swap.animate}
              exit={reduce ? { opacity: 0 } : swap.exit}
              transition={reduce ? { duration: 0 } : { duration: duration.base, ease: ease.out }}
              className="col-start-1 row-start-1 justify-self-center"
            >
              {message}
            </motion.p>
          </AnimatePresence>
        </div>
      </div>

      <div aria-hidden className="-mb-6 h-(--size-control-sm)" />
    </div>
  );
}
```

### PIN

type="password" draws a dot for each digit. Turn autocomplete off for a PIN.

```tsx
import { InputOTP, InputOTPGroup, InputOTPSlot } from "@/components/ui/input-otp";
import { useId } from "react";

/** A four-digit PIN: type="password" draws a dot for each digit, and autocomplete is off */
export default function InputOTPPin() {
  const id = useId();
  return (
    <div className="grid justify-items-center gap-3">
      <span id={id} className="text-sm font-medium">
        PIN
      </span>
      <InputOTP type="password" autoComplete="off" aria-labelledby={id}>
        <InputOTPGroup>
          <InputOTPSlot index={0} />
          <InputOTPSlot index={1} />
          <InputOTPSlot index={2} />
          <InputOTPSlot index={3} />
        </InputOTPGroup>
      </InputOTP>
    </div>
  );
}
```

## Related

- [Input](https://ui.fewtone.com/components/input.md): An accessible React text input with three sizes and clear invalid, disabled and file states, built on a native input.
- [Number Input](https://ui.fewtone.com/components/number-input.md): An accessible React number input for Next.js and Tailwind with step buttons, keyboard control, min and max, and Intl number formatting.
- [Button](https://ui.fewtone.com/components/button.md): An accessible React button for Next.js and Tailwind CSS with six variants, three sizes, icons and a soft tint on hover and press, built on a native button.
