# OTP Field

Enter a short verification code in separate or joined slots.

Verification code

[Complete TSX file](/examples/otp-field/default.tsx)

```tsx
"use client";

import {
  Field,
  FieldDescription,
  FieldLabel,
} from "@/siteplane/components/field";
import { OTPField, OTPFieldInput } from "@/siteplane/components/otp-field";

export default function Example() {
  return (
    <Field className="items-center">
      <FieldLabel>Verification code</FieldLabel>
      <OTPField length={6}>
        {Array.from({ length: 6 }, (_, index) => (
          <OTPFieldInput
            key={index}
            aria-label={index === 0 ? undefined : `Digit ${index + 1} of 6`}
          />
        ))}
      </OTPField>
      <FieldDescription>
        Enter the 6-digit code we sent to your email.
      </FieldDescription>
    </Field>
  );
}
```

## Installation

Complete [one-time setup](/docs/installation/) first.

```bash
pnpm dlx shadcn@4.18.0 add @siteplane/otp-field
```

With npm: `npx shadcn@4.18.0 add @siteplane/otp-field`. [Registry JSON](/registry/otp-field.json) lists source files and dependencies.

For this page’s examples, also install:

```bash
pnpm dlx shadcn@4.18.0 add @siteplane/button @siteplane/form @siteplane/input
```

Field is installed with Input.

## Usage

```tsx
import { OTPField, OTPFieldInput } from "@/siteplane/components/otp-field";

<OTPField length={4} aria-label="Verification code">
  {Array.from({ length: 4 }, (_, index) => (
    <OTPFieldInput
      key={index}
      aria-label={index === 0 ? undefined : `Digit ${index + 1} of 4`}
    />
  ))}
</OTPField>;
```

## Composition

```text
OTPField
├── OTPFieldInput × length
└── OTPFieldSeparator (optional between slots)
```

## Grouped slots

Separators do not count toward length. Render exactly the declared number of inputs.

Verification code in two groups

[Complete TSX file](/examples/otp-field/separator.tsx)

```tsx
"use client";

import {
  OTPField,
  OTPFieldInput,
  OTPFieldSeparator,
} from "@/siteplane/components/otp-field";

export default function Example() {
  return (
    <OTPField length={6} aria-label="Verification code">
      <OTPFieldInput />
      <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>
  );
}
```

## Joined slots

Set `layout="joined"` to draw the slots as one field instead of separate boxes. The field has the surface and height of an [Input](/docs/components/input/), follows `size` and `variant`, and separates the characters with thin dividers. States follow the separate boxes: the current slot takes the focus ring inside the field while the field keeps its edge, no slot shows a caret, an invalid field shows the invalid edge, and a disabled field fades.

A joined field fills the width of its container, like an Input. Set a width on `OTPField`, or on the `Field` around it, to size it. `OTPFieldSeparator` still splits the slots into groups.

Verification code in a joined field

[Complete TSX file](/examples/otp-field/joined.tsx)

```tsx
"use client";

import {
  Field,
  FieldDescription,
  FieldLabel,
} from "@/siteplane/components/field";
import { OTPField, OTPFieldInput } from "@/siteplane/components/otp-field";

export default function Example() {
  return (
    <Field className="w-full max-w-72">
      <FieldLabel>Verification code</FieldLabel>
      <OTPField layout="joined" length={6}>
        {Array.from({ length: 6 }, (_, index) => (
          <OTPFieldInput
            key={index}
            aria-label={index === 0 ? undefined : `Digit ${index + 1} of 6`}
          />
        ))}
      </OTPField>
      <FieldDescription>
        Enter the 6-digit code we sent to your email.
      </FieldDescription>
    </Field>
  );
}
```

## Alphanumeric codes

Set `validationType="alphanumeric"` to accept letters and digits, for example for recovery codes.

Recovery code

[Complete TSX file](/examples/otp-field/alphanumeric.tsx)

```tsx
"use client";

import {
  Field,
  FieldDescription,
  FieldLabel,
} from "@/siteplane/components/field";
import { OTPField, OTPFieldInput } from "@/siteplane/components/otp-field";

export default function Example() {
  return (
    <Field className="w-full max-w-sm items-center">
      <FieldLabel>Recovery code</FieldLabel>
      <OTPField length={6} validationType="alphanumeric">
        {Array.from({ length: 6 }, (_, index) => (
          <OTPFieldInput
            key={index}
            aria-label={index === 0 ? undefined : `Character ${index + 1} of 6`}
          />
        ))}
      </OTPField>
      <FieldDescription className="text-center">
        Enter one of the backup codes you saved when you turned on two-factor
        authentication.
      </FieldDescription>
    </Field>
  );
}
```

## Controlled value

Use `value` and `onValueChange` to own the code. `onValueComplete` fires once every slot is filled; verify the code in your application.

Controlled one-time password

[Complete TSX file](/examples/otp-field/controlled.tsx)

```tsx
"use client";

import {
  Field,
  FieldDescription,
  FieldLabel,
} from "@/siteplane/components/field";
import { OTPField, OTPFieldInput } from "@/siteplane/components/otp-field";
import { useState } from "react";

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

  return (
    <Field className="items-center">
      <FieldLabel>One-time password</FieldLabel>
      <OTPField length={6} value={value} onValueChange={setValue}>
        {Array.from({ length: 6 }, (_, index) => (
          <OTPFieldInput
            key={index}
            aria-label={index === 0 ? undefined : `Digit ${index + 1} of 6`}
          />
        ))}
      </OTPField>
      <FieldDescription>
        Enter the code from your authenticator app.
      </FieldDescription>
    </Field>
  );
}
```

## Variants, masking and disabled

Masking only hides the displayed characters. Disabled and readOnly are separate states.

Code variants, masking and disabled

[Complete TSX file](/examples/otp-field/appearance.tsx)

```tsx
"use client";

import { Field, FieldLabel } from "@/siteplane/components/field";
import { OTPField, OTPFieldInput } from "@/siteplane/components/otp-field";

const fields: {
  defaultValue?: string;
  disabled?: boolean;
  label: string;
  mask?: boolean;
  variant?: "default" | "elevated" | "inset";
}[] = [
  { label: "Verification code, default", variant: "default" },
  {
    defaultValue: "1234",
    label: "PIN, elevated and masked",
    mask: true,
    variant: "elevated",
  },
  { label: "Verification code, inset", variant: "inset" },
  {
    defaultValue: "1234",
    label: "Verification code, disabled",
    disabled: true,
  },
];

export default function Example() {
  const slots = Array.from({ length: 4 }, (_, index) => (
    <OTPFieldInput
      key={index}
      aria-label={index === 0 ? undefined : `Digit ${index + 1} of 4`}
    />
  ));

  return (
    <div className="grid gap-4">
      {fields.map(({ defaultValue, disabled, label, mask, variant }) => (
        <Field disabled={disabled} key={label}>
          <FieldLabel>{label}</FieldLabel>
          <OTPField
            defaultValue={defaultValue}
            length={4}
            mask={mask}
            variant={variant}
          >
            {slots}
          </OTPField>
        </Field>
      ))}
    </div>
  );
}
```

## Sizes

Omit `size` to inherit the provider's control size.

Code sizes

[Complete TSX file](/examples/otp-field/sizes.tsx)

```tsx
"use client";

import { Field, FieldLabel } from "@/siteplane/components/field";
import { OTPField, OTPFieldInput } from "@/siteplane/components/otp-field";

const sizes = [
  { size: "sm", label: "small" },
  { size: "md", label: "medium" },
  { size: "lg", label: "large" },
  { size: "xl", label: "extra large" },
] as const;

export default function Example() {
  const slots = Array.from({ length: 4 }, (_, index) => (
    <OTPFieldInput
      key={index}
      aria-label={index === 0 ? undefined : `Digit ${index + 1} of 4`}
    />
  ));

  return (
    <div className="grid gap-4">
      {sizes.map(({ size, label }) => (
        <Field key={size}>
          <FieldLabel>Verification code, {label}</FieldLabel>
          <OTPField length={4} size={size}>
            {slots}
          </OTPField>
        </Field>
      ))}
    </div>
  );
}
```

## Form integration

Give the Field a name to submit the complete code. `autoSubmit` can submit the form once all slots are filled.

Verification code form

[Complete TSX file](/examples/otp-field/form.tsx)

```tsx
"use client";

import { Button } from "@/siteplane/components/button";
import { Field, FieldError, FieldLabel } from "@/siteplane/components/field";
import { Form } from "@/siteplane/components/form";
import { OTPField, OTPFieldInput } from "@/siteplane/components/otp-field";

export default function Example() {
  return (
    <Form
      className="flex flex-col items-start gap-4"
      onSubmit={(event) => event.preventDefault()}
    >
      <Field name="code">
        <FieldLabel>Verification code</FieldLabel>
        <OTPField length={6} required>
          {Array.from({ length: 6 }, (_, index) => (
            <OTPFieldInput
              key={index}
              aria-label={index === 0 ? undefined : `Digit ${index + 1} of 6`}
            />
          ))}
        </OTPField>
        <FieldError>Enter the 6-digit code.</FieldError>
      </Field>
      <Button type="submit">Verify</Button>
    </Form>
  );
}
```

## API reference

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| OTPField.layout | separate \| joined | separate | One box per slot, or one joined field with the surface of an Input and a divider between the slots. |
| OTPField.size | sm \| md \| lg \| xl | provider / lg | Slot dimensions; the height of a joined field. |
| OTPField.variant | default \| elevated \| inset | default | Surface of each slot, or of the joined field. |
| OTPField.aria-label | string | unset | Creates a hidden Label for the first input unless aria-labelledby is supplied. |
| OTPField.id | string | generated when aria-label exists | First input ID. |
| OTPFieldInput.spellCheck | boolean | false | Spelling checks disabled unless overridden. |
| length | number | required | Number of input slots. |
| value / defaultValue | string | uncontrolled | Complete code. |
| onValueChange | (value, eventDetails) => void | unset | Code-change callback. |
| onValueComplete | (value, eventDetails) => void | unset | Called on completion; does not authenticate. |
| onValueInvalid | (value, eventDetails) => void | unset | Reports characters rejected during normalization. |
| validationType | OTP validation mode | numeric | Allowed characters. |
| mask / autoSubmit | boolean | false | Mask slots or submit on completion. |
`OTPFieldProps`, `OTPFieldInputProps` and `OTPFieldSeparatorProps` describe
these parts. `OTPFieldPrimitive` exposes Root, Input and the Base UI Separator.
Styled Separator uses a short horizontal divider and accepts Separator props. A
joined field draws the dividers between its slots itself. Other normalization,
required, readOnly, disabled, autocomplete, name and form contracts follow [Base
UI OTP Field](https://base-ui.com/react/components/otp-field). Styled className
values are strings.

[Read the installed source](/source/otp-field).

## Usage notes

Typing, pasting, deletion and slot focus are handled by the component. Give the field and slots meaningful names: the first slot takes the field's label and ignores its own `aria-label`, so label only the following slots. Never treat a client-side completion callback as verification; server validation and retry/rate limits belong to the application.
