---
title: OTP Field
subtitle: A one-time password input composed of individual character slots.
description: A high-quality, unstyled React OTP field component for one-time password and verification code entry.
---

> FineSoft Components documentation. Independent Stencil port; not the official Base UI website.
>
> React API text and `@base-ui/react` examples are upstream references. FineSoft Stencil examples use `finesoft-components`. Do not treat the two packages as interchangeable.

# OTP Field

A high-quality, unstyled React OTP field component for one-time password and verification code entry.

## Demo

### Tailwind

This example shows how to implement the component using Tailwind CSS.

```tsx
/* docs-comparison.tsx */
import { StencilComparison } from 'docs/src/components/StencilComparison';
import ReactOTPField from './react-otp-field';
import StencilOTPField from './stencil-otp-field';

export default function OTPFieldComparison() {
  return <StencilComparison key="tailwind" component="otp-field" react={<ReactOTPField />} stencil={<StencilOTPField />} />;
}
```

```tsx
/* react-otp-field.tsx */
import * as React from 'react';
import { OTPField } from '@base-ui/react/otp-field';

const OTP_LENGTH = 6;

export default function ExampleOTPField() {
  const id = React.useId();
  const descriptionId = `${id}-description`;

  return (
    <div className="flex w-full max-w-80 flex-col items-start gap-1">
      <label htmlFor={id} className="text-sm font-bold text-neutral-950 dark:text-white">
        Verification code
      </label>
      <OTPField.Root id={id} length={OTP_LENGTH} aria-describedby={descriptionId} className="flex w-full gap-2">
        {Array.from({ length: OTP_LENGTH }, (_, index) => (
          <OTPField.Input
            key={index}
            className="m-0 h-10 w-10 rounded-none border border-neutral-950 bg-white dark:bg-neutral-950 text-center font-inherit text-base font-normal text-neutral-950 focus:outline-2 focus:-outline-offset-1 focus:outline-neutral-950 dark:focus:outline-white dark:border-white dark:text-white"
            aria-label={index === 0 ? undefined : `Character ${index + 1} of ${OTP_LENGTH}`}
          />
        ))}
      </OTPField.Root>
      <p id={descriptionId} className="m-0 text-sm text-neutral-600 dark:text-neutral-400">
        Enter the 6-character code we sent to your device.
      </p>
    </div>
  );
}
```

```tsx
/* stencil-otp-field.tsx */
import * as React from 'react';

const OTP_LENGTH = 6;

export default function StencilOTPField() {
  const id = `stencil-hero-${React.useId()}`;
  const descriptionId = `${id}-description`;

  return (
    <div className="flex w-full max-w-80 flex-col items-start gap-1">
      <label htmlFor={id} className="text-sm font-bold text-neutral-950 dark:text-white">
        Verification code
      </label>
      <fs-otp-field-root id={id} length={OTP_LENGTH} aria-describedby={descriptionId} className="flex w-full gap-2">
        {Array.from({ length: OTP_LENGTH }, (_, index) => (
          <fs-otp-field-input
            key={index}
            className="m-0 h-10 w-10 rounded-none border border-neutral-950 bg-white dark:bg-neutral-950 text-center font-inherit text-base font-normal text-neutral-950 focus:outline-2 focus:-outline-offset-1 focus:outline-neutral-950 dark:focus:outline-white dark:border-white dark:text-white"
            aria-label={index === 0 ? undefined : `Character ${index + 1} of ${OTP_LENGTH}`}
          />
        ))}
      </fs-otp-field-root>
      <p id={descriptionId} className="m-0 text-sm text-neutral-600 dark:text-neutral-400">
        Enter the 6-character code we sent to your device.
      </p>
    </div>
  );
}
```

### CSS Modules

This example shows how to implement the component using CSS Modules.

```css
/* index.module.css */
.Field {
  display: flex;
  flex-direction: column;
  align-items: start;
  gap: 0.25rem;
  width: 100%;
  max-width: 20rem;
}

.Label {
  font-size: 0.875rem;
  line-height: 1.25rem;
  font-weight: 700;
  color: oklch(14.5% 0 0deg);

  @media (prefers-color-scheme: dark) {
    color: white;
  }
}

.Root {
  display: flex;
  gap: 0.5rem;
  width: 100%;
}

.Input {
  box-sizing: border-box;
  margin: 0;
  padding: 0;

  border: 1px solid oklch(14.5% 0 0deg);
  width: 2.5rem;
  height: 2.5rem;
  border-radius: 0;
  font-family: inherit;
  font-size: 1rem;
  line-height: 1.5rem;
  font-weight: 400;
  text-align: center;
  background-color: white;
  color: oklch(14.5% 0 0deg);

  @media (prefers-color-scheme: dark) {
    border: 1px solid white;
    background-color: oklch(14.5% 0 0deg);
    color: white;
  }

  &:focus {
    outline: 2px solid oklch(14.5% 0 0deg);
    outline-offset: -1px;

    @media (prefers-color-scheme: dark) {
      outline-color: white;
    }
  }
}

.Description {
  margin: 0;
  font-size: 0.875rem;
  line-height: 1.25rem;
  color: oklch(43.9% 0 0deg);

  @media (prefers-color-scheme: dark) {
    color: oklch(70.8% 0 0deg);
  }
}
```

```tsx
/* docs-comparison.tsx */
import { StencilComparison } from 'docs/src/components/StencilComparison';
import ReactOTPField from './react-otp-field';
import StencilOTPField from './stencil-otp-field';

export default function OTPFieldComparison() {
  return <StencilComparison key="css-modules" component="otp-field" react={<ReactOTPField />} stencil={<StencilOTPField />} />;
}
```

```tsx
/* react-otp-field.tsx */
import * as React from 'react';
import { OTPField } from '@base-ui/react/otp-field';
import styles from './index.module.css';

const OTP_LENGTH = 6;

export default function ExampleOTPField() {
  const id = React.useId();
  const descriptionId = `${id}-description`;

  return (
    <div className={styles.Field}>
      <label htmlFor={id} className={styles.Label}>
        Verification code
      </label>
      <OTPField.Root id={id} length={OTP_LENGTH} aria-describedby={descriptionId} className={styles.Root}>
        {Array.from({ length: OTP_LENGTH }, (_, index) => (
          <OTPField.Input key={index} className={styles.Input} aria-label={index === 0 ? undefined : `Character ${index + 1} of ${OTP_LENGTH}`} />
        ))}
      </OTPField.Root>
      <p id={descriptionId} className={styles.Description}>
        Enter the 6-character code we sent to your device.
      </p>
    </div>
  );
}
```

```tsx
/* stencil-otp-field.tsx */
import * as React from 'react';
import './stencil.css';

const OTP_LENGTH = 6;

export default function StencilOTPField() {
  const id = `stencil-hero-${React.useId()}`;
  const descriptionId = `${id}-description`;

  return (
    <div className="demo-otp-field-hero-field">
      <label htmlFor={id}>Verification code</label>
      <fs-otp-field-root id={id} length={OTP_LENGTH} aria-describedby={descriptionId}>
        {Array.from({ length: OTP_LENGTH }, (_, index) => (
          <fs-otp-field-input key={index} aria-label={index === 0 ? undefined : `Character ${index + 1} of ${OTP_LENGTH}`} />
        ))}
      </fs-otp-field-root>
      <p id={descriptionId}>Enter the 6-character code we sent to your device.</p>
    </div>
  );
}
```

```css
/* stencil.css */
/* Example layout and visual exceptions. Default component styles come from the theme. */
@layer fs-theme {
  .demo-otp-field-hero-field {
    display: flex;
    flex-direction: column;
    align-items: start;
    gap: var(--fs-space-1);
    width: 100%;
    max-width: 20rem;
  }
}
```

## Stencil implementation

The comparisons use the frozen Base UI 1.7.0 examples alongside these public custom elements:

| Base UI part         | Stencil element          |
| -------------------- | ------------------------ |
| `OTPField.Root`      | `fs-otp-field-root`      |
| `OTPField.Input`     | `fs-otp-field-input`     |
| `OTPField.Separator` | `fs-otp-field-separator` |

Import `finesoft-components/otp-field` to register the elements automatically, or call
`defineCustomElements()` from `finesoft-components/define-custom-elements` once. This site supplies
that shared registration and component-generated SSR output outside the examples.

Use `value`, `defaultValue`, `length`, `normalizeValue` and the other Root properties directly.
For static HTML, multiword attributes use kebab case (`default-value`, `validation-type`,
`auto-submit`); native attributes retain their native spelling (`autocomplete`, `inputmode`, `readonly`).
`valueChange`, `valueInvalid` and `valueComplete` are bubbling, composed DOM CustomEvents. Their
`detail` includes `value`, `reason` and the original native `event`. A controlled consumer writes
accepted values back to `root.value`. `valueChange` supports `preventDefault()` and `detail.cancel()`;
the invalid and complete notifications are not cancelable, matching the original generic callbacks.
Completion follows the accepted value update and immediately precedes `autoSubmit`.

Root owns a native group and a visually hidden text input for the full form value. Its standard
`id` labels the first visible input; subsequent inputs derive `-2`, `-3`, etc. Input hosts are
transparent and forward consumer styles and native input attributes to their real inputs.
Separator is an independent semantic separator, just like the original alias.

The examples below run exactly the visible `stencil-*` source files. Custom normalization also
shows its reusable view and invalid-feedback hook; there is no docs-only behavior controller.
The Stencil implementation does not consume React context or the React `render`/render-function
APIs. Native labels, native form participation, and [Field](/react/components/field.md) integration are
supported. The migrated [Form](/react/components/form.md) adds inherited validation mode,
consolidated values, form-level errors, and submit validation when OTP Field is used through Field.

## Usage guidelines

- **Form controls must have an accessible name**: It can be created using a `<label>` element or the `Field` component. See [Labeling an OTP field](/react/components/otp-field.md) and the [forms guide](/react/handbook/forms.md).

## Anatomy

Import the component and assemble its parts:

```jsx title="Anatomy"
import { OTPField } from '@base-ui/react/otp-field';

<OTPField.Root>
  <OTPField.Input />
  <OTPField.Separator />
</OTPField.Root>;
```

## Examples

### Labeling an OTP field

Pass an `id` to `<OTPField.Root>` and use a native `<label>` with a matching `htmlFor`. Let the
first input use the field label, and add `aria-label` to the remaining inputs so assistive
technology can announce which slot is focused.

Optionally, add `aria-describedby` when supporting text should be announced with the field.

```tsx title="OTP Field with a native label and description"
<div>
  <label htmlFor="verification-code">Verification code</label>
  <OTPField.Root id="verification-code" length={6} aria-describedby="verification-code-description">
    <OTPField.Input />
    <OTPField.Input aria-label="Character 2 of 6" />
    <OTPField.Input aria-label="Character 3 of 6" />
    <OTPField.Input aria-label="Character 4 of 6" />
    <OTPField.Input aria-label="Character 5 of 6" />
    <OTPField.Input aria-label="Character 6 of 6" />
  </OTPField.Root>
  <p id="verification-code-description">Enter the 6-character code we sent to your device.</p>
</div>
```

### Form integration

Use [Field](/react/components/field.md) to handle label associations and form integration:

```tsx title="Using OTP Field in a form" {2}
<Form>
  <Field.Root name="verificationCode">
    <Field.Label>Verification code</Field.Label>
    <Field.Description>Enter the 6-character code we sent to your device.</Field.Description>
    <OTPField.Root length={6}>
      <OTPField.Input />
      <OTPField.Input aria-label="Character 2 of 6" />
      <OTPField.Input aria-label="Character 3 of 6" />
      <OTPField.Input aria-label="Character 4 of 6" />
      <OTPField.Input aria-label="Character 5 of 6" />
      <OTPField.Input aria-label="Character 6 of 6" />
    </OTPField.Root>
  </Field.Root>
</Form>
```

Pass `autoSubmit` to submit the owning form automatically when all slots are filled, or use
`onValueComplete` to react to completion without submitting.

### Alphanumeric verification codes

Use `validationType="alphanumeric"` for recovery, backup, or invite codes that mix letters and
numbers.

## Demo

### Tailwind

This example shows how to implement the component using Tailwind CSS.

```tsx
/* docs-comparison.tsx */
import { StencilComparison } from 'docs/src/components/StencilComparison';
import ReactOTPField from './react-otp-field';
import StencilOTPField from './stencil-otp-field';

export default function OTPFieldComparison() {
  return <StencilComparison key="tailwind" component="otp-field" react={<ReactOTPField />} stencil={<StencilOTPField />} />;
}
```

```tsx
/* react-otp-field.tsx */
import * as React from 'react';
import { OTPField } from '@base-ui/react/otp-field';

const CODE_LENGTH = 6;

export default function OTPFieldAlphanumericDemo() {
  const id = React.useId();
  const descriptionId = `${id}-description`;

  return (
    <div className="flex w-full max-w-80 flex-col items-start gap-1">
      <label htmlFor={id} className="text-sm font-bold text-neutral-950 dark:text-white">
        Recovery code
      </label>
      <OTPField.Root id={id} length={CODE_LENGTH} validationType="alphanumeric" aria-describedby={descriptionId} className="flex w-full gap-2">
        {Array.from({ length: CODE_LENGTH }, (_, index) => (
          <OTPField.Input
            key={index}
            className="m-0 h-10 w-10 rounded-none border border-neutral-950 bg-white dark:bg-neutral-950 text-center font-inherit text-base font-normal text-neutral-950 focus:outline-2 focus:-outline-offset-1 focus:outline-neutral-950 dark:focus:outline-white dark:border-white dark:text-white"
            aria-label={index === 0 ? undefined : `Character ${index + 1} of ${CODE_LENGTH}`}
          />
        ))}
      </OTPField.Root>
      <p id={descriptionId} className="m-0 text-sm text-neutral-600 dark:text-neutral-400">
        Accept letters and numbers for backup codes such as <code className="font-mono">A7C9XZ</code>.
      </p>
    </div>
  );
}
```

```tsx
/* stencil-otp-field.tsx */
import * as React from 'react';

const CODE_LENGTH = 6;

export default function StencilOTPField() {
  const id = `stencil-alphanumeric-${React.useId()}`;
  const descriptionId = `${id}-description`;

  return (
    <div className="flex w-full max-w-80 flex-col items-start gap-1">
      <label htmlFor={id} className="text-sm font-bold text-neutral-950 dark:text-white">
        Recovery code
      </label>
      <fs-otp-field-root id={id} length={CODE_LENGTH} validation-type="alphanumeric" aria-describedby={descriptionId} className="flex w-full gap-2">
        {Array.from({ length: CODE_LENGTH }, (_, index) => (
          <fs-otp-field-input
            key={index}
            className="m-0 h-10 w-10 rounded-none border border-neutral-950 bg-white dark:bg-neutral-950 text-center font-inherit text-base font-normal text-neutral-950 focus:outline-2 focus:-outline-offset-1 focus:outline-neutral-950 dark:focus:outline-white dark:border-white dark:text-white"
            aria-label={index === 0 ? undefined : `Character ${index + 1} of ${CODE_LENGTH}`}
          />
        ))}
      </fs-otp-field-root>
      <p id={descriptionId} className="m-0 text-sm text-neutral-600 dark:text-neutral-400">
        Accept letters and numbers for backup codes such as <code className="font-mono">A7C9XZ</code>.
      </p>
    </div>
  );
}
```

### CSS Modules

This example shows how to implement the component using CSS Modules.

```css
/* index.module.css */
.Field {
  display: flex;
  flex-direction: column;
  align-items: start;
  gap: 0.25rem;
  width: 100%;
  max-width: 20rem;
}

.Label {
  font-size: 0.875rem;
  line-height: 1.25rem;
  font-weight: 700;
  color: oklch(14.5% 0 0deg);

  @media (prefers-color-scheme: dark) {
    color: white;
  }
}

.Root {
  display: flex;
  gap: 0.5rem;
  width: 100%;
}

.Input {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
  border: 1px solid oklch(14.5% 0 0deg);
  width: 2.5rem;
  height: 2.5rem;
  border-radius: 0;
  font-family: inherit;
  font-size: 1rem;
  line-height: 1.5rem;
  font-weight: 400;
  text-align: center;
  background-color: white;
  color: oklch(14.5% 0 0deg);

  @media (prefers-color-scheme: dark) {
    border: 1px solid white;
    background-color: oklch(14.5% 0 0deg);
    color: white;
  }

  &:focus {
    outline: 2px solid oklch(14.5% 0 0deg);
    outline-offset: -1px;

    @media (prefers-color-scheme: dark) {
      outline-color: white;
    }
  }
}

.Description {
  margin: 0;
  font-size: 0.875rem;
  line-height: 1.25rem;
  color: oklch(43.9% 0 0deg);

  @media (prefers-color-scheme: dark) {
    color: oklch(70.8% 0 0deg);
  }
}

.Code {
  font-family: 'Paper Mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
}
```

```tsx
/* docs-comparison.tsx */
import { StencilComparison } from 'docs/src/components/StencilComparison';
import ReactOTPField from './react-otp-field';
import StencilOTPField from './stencil-otp-field';

export default function OTPFieldComparison() {
  return <StencilComparison key="css-modules" component="otp-field" react={<ReactOTPField />} stencil={<StencilOTPField />} />;
}
```

```tsx
/* react-otp-field.tsx */
import * as React from 'react';
import { OTPField } from '@base-ui/react/otp-field';
import styles from './index.module.css';

const CODE_LENGTH = 6;

export default function OTPFieldAlphanumericDemo() {
  const id = React.useId();
  const descriptionId = `${id}-description`;

  return (
    <div className={styles.Field}>
      <label htmlFor={id} className={styles.Label}>
        Recovery code
      </label>
      <OTPField.Root id={id} length={CODE_LENGTH} validationType="alphanumeric" aria-describedby={descriptionId} className={styles.Root}>
        {Array.from({ length: CODE_LENGTH }, (_, index) => (
          <OTPField.Input key={index} className={styles.Input} aria-label={index === 0 ? undefined : `Character ${index + 1} of ${CODE_LENGTH}`} />
        ))}
      </OTPField.Root>
      <p id={descriptionId} className={styles.Description}>
        Accept letters and numbers for backup codes such as <span className={styles.Code}>A7C9XZ</span>.
      </p>
    </div>
  );
}
```

```tsx
/* stencil-otp-field.tsx */
import * as React from 'react';
import './stencil.css';

const CODE_LENGTH = 6;

export default function StencilOTPField() {
  const id = `stencil-alphanumeric-${React.useId()}`;
  const descriptionId = `${id}-description`;

  return (
    <div className="demo-otp-field-alphanumeric-field">
      <label htmlFor={id}>Recovery code</label>
      <fs-otp-field-root id={id} length={CODE_LENGTH} validation-type="alphanumeric" aria-describedby={descriptionId}>
        {Array.from({ length: CODE_LENGTH }, (_, index) => (
          <fs-otp-field-input key={index} aria-label={index === 0 ? undefined : `Character ${index + 1} of ${CODE_LENGTH}`} />
        ))}
      </fs-otp-field-root>
      <p id={descriptionId}>
        Accept letters and numbers for backup codes such as <span className="demo-otp-field-alphanumeric-code">A7C9XZ</span>.
      </p>
    </div>
  );
}
```

```css
/* stencil.css */
/* Example layout and visual exceptions. Default component styles come from the theme. */
@layer fs-theme {
  .demo-otp-field-alphanumeric-field {
    display: flex;
    flex-direction: column;
    align-items: start;
    gap: var(--fs-space-1);
    width: 100%;
    max-width: 20rem;
  }

  .demo-otp-field-alphanumeric-code {
    font-family: 'Paper Mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
  }
}
```

### Grouped layouts

Wrap subsets of inputs in your own layout elements and use `<OTPField.Separator>` when you
want the code presented in smaller visual chunks such as `123-456`.

## Demo

### Tailwind

This example shows how to implement the component using Tailwind CSS.

```tsx
/* docs-comparison.tsx */
import { StencilComparison } from 'docs/src/components/StencilComparison';
import ReactOTPField from './react-otp-field';
import StencilOTPField from './stencil-otp-field';

export default function OTPFieldComparison() {
  return <StencilComparison key="tailwind" component="otp-field" react={<ReactOTPField />} stencil={<StencilOTPField />} />;
}
```

```tsx
/* react-otp-field.tsx */
import * as React from 'react';
import { OTPField } from '@base-ui/react/otp-field';

const OTP_LENGTH = 6;

export default function OTPFieldGroupedDemo() {
  const id = React.useId();

  return (
    <div className="flex w-full max-w-80 flex-col items-start gap-1">
      <label htmlFor={id} className="text-sm font-bold text-neutral-950 dark:text-white">
        Verification code
      </label>
      <OTPField.Root id={id} length={OTP_LENGTH} className="flex w-full items-center gap-2">
        <div className="flex gap-2">
          {Array.from({ length: 3 }, (_, index) => (
            <OTPField.Input
              key={index}
              className="m-0 h-10 w-10 rounded-none border border-neutral-950 bg-white dark:bg-neutral-950 text-center font-inherit text-base font-normal text-neutral-950 focus:outline-2 focus:-outline-offset-1 focus:outline-neutral-950 dark:focus:outline-white dark:border-white dark:text-white"
              aria-label={index === 0 ? undefined : `Character ${index + 1} of ${OTP_LENGTH}`}
            />
          ))}
        </div>
        <OTPField.Separator className="h-px w-4 bg-current text-neutral-950 dark:text-white" />
        <div className="flex gap-2">
          {Array.from({ length: 3 }, (_, index) => (
            <OTPField.Input
              key={index + 3}
              className="m-0 h-10 w-10 rounded-none border border-neutral-950 bg-white dark:bg-neutral-950 text-center font-inherit text-base font-normal text-neutral-950 focus:outline-2 focus:-outline-offset-1 focus:outline-neutral-950 dark:focus:outline-white dark:border-white dark:text-white"
              aria-label={`Character ${index + 4} of ${OTP_LENGTH}`}
            />
          ))}
        </div>
      </OTPField.Root>
    </div>
  );
}
```

```tsx
/* stencil-otp-field.tsx */
import * as React from 'react';

const OTP_LENGTH = 6;

export default function StencilOTPField() {
  const id = `stencil-grouped-${React.useId()}`;

  return (
    <div className="flex w-full max-w-80 flex-col items-start gap-1">
      <label htmlFor={id} className="text-sm font-bold text-neutral-950 dark:text-white">
        Verification code
      </label>
      <fs-otp-field-root id={id} length={OTP_LENGTH} className="flex w-full items-center gap-2">
        <div className="flex gap-2">
          {Array.from({ length: 3 }, (_, index) => (
            <fs-otp-field-input
              key={index}
              className="m-0 h-10 w-10 rounded-none border border-neutral-950 bg-white dark:bg-neutral-950 text-center font-inherit text-base font-normal text-neutral-950 focus:outline-2 focus:-outline-offset-1 focus:outline-neutral-950 dark:focus:outline-white dark:border-white dark:text-white"
              aria-label={index === 0 ? undefined : `Character ${index + 1} of ${OTP_LENGTH}`}
            />
          ))}
        </div>
        <fs-otp-field-separator className="h-px w-4 bg-current text-neutral-950 dark:text-white" />
        <div className="flex gap-2">
          {Array.from({ length: 3 }, (_, index) => (
            <fs-otp-field-input
              key={index + 3}
              className="m-0 h-10 w-10 rounded-none border border-neutral-950 bg-white dark:bg-neutral-950 text-center font-inherit text-base font-normal text-neutral-950 focus:outline-2 focus:-outline-offset-1 focus:outline-neutral-950 dark:focus:outline-white dark:border-white dark:text-white"
              aria-label={`Character ${index + 4} of ${OTP_LENGTH}`}
            />
          ))}
        </div>
      </fs-otp-field-root>
    </div>
  );
}
```

### CSS Modules

This example shows how to implement the component using CSS Modules.

```css
/* index.module.css */
.Field {
  display: flex;
  flex-direction: column;
  align-items: start;
  gap: 0.25rem;
  width: 100%;
  max-width: 20rem;
}

.Label {
  font-size: 0.875rem;
  line-height: 1.25rem;
  font-weight: 700;
  color: oklch(14.5% 0 0deg);

  @media (prefers-color-scheme: dark) {
    color: white;
  }
}

.Root {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  width: 100%;
}

.Group {
  display: flex;
  gap: 0.5rem;
}

.Input {
  box-sizing: border-box;
  margin: 0;
  padding: 0;

  border: 1px solid oklch(14.5% 0 0deg);
  width: 2.5rem;
  height: 2.5rem;
  border-radius: 0;
  font-family: inherit;
  font-size: 1rem;
  line-height: 1.5rem;
  font-weight: 400;
  text-align: center;
  background-color: white;
  color: oklch(14.5% 0 0deg);

  @media (prefers-color-scheme: dark) {
    border: 1px solid white;
    background-color: oklch(14.5% 0 0deg);
    color: white;
  }

  &:focus {
    outline: 2px solid oklch(14.5% 0 0deg);
    outline-offset: -1px;

    @media (prefers-color-scheme: dark) {
      outline-color: white;
    }
  }
}

.Separator {
  width: 1rem;
  height: 1px;
  background-color: currentColor;
  color: oklch(14.5% 0 0deg);

  @media (prefers-color-scheme: dark) {
    color: white;
  }
}
```

```tsx
/* docs-comparison.tsx */
import { StencilComparison } from 'docs/src/components/StencilComparison';
import ReactOTPField from './react-otp-field';
import StencilOTPField from './stencil-otp-field';

export default function OTPFieldComparison() {
  return <StencilComparison key="css-modules" component="otp-field" react={<ReactOTPField />} stencil={<StencilOTPField />} />;
}
```

```tsx
/* react-otp-field.tsx */
import * as React from 'react';
import { OTPField } from '@base-ui/react/otp-field';
import styles from './index.module.css';

const OTP_LENGTH = 6;

export default function OTPFieldGroupedDemo() {
  const id = React.useId();

  return (
    <div className={styles.Field}>
      <label htmlFor={id} className={styles.Label}>
        Verification code
      </label>
      <OTPField.Root id={id} length={OTP_LENGTH} className={styles.Root}>
        <div className={styles.Group}>
          {Array.from({ length: 3 }, (_, index) => (
            <OTPField.Input key={index} className={styles.Input} aria-label={index === 0 ? undefined : `Character ${index + 1} of ${OTP_LENGTH}`} />
          ))}
        </div>
        <OTPField.Separator className={styles.Separator} />
        <div className={styles.Group}>
          {Array.from({ length: 3 }, (_, index) => (
            <OTPField.Input key={index + 3} className={styles.Input} aria-label={`Character ${index + 4} of ${OTP_LENGTH}`} />
          ))}
        </div>
      </OTPField.Root>
    </div>
  );
}
```

```tsx
/* stencil-otp-field.tsx */
import * as React from 'react';
import './stencil.css';

const OTP_LENGTH = 6;

export default function StencilOTPField() {
  const id = `stencil-grouped-${React.useId()}`;

  return (
    <div className="demo-otp-field-grouped-field">
      <label htmlFor={id}>Verification code</label>
      <fs-otp-field-root id={id} length={OTP_LENGTH}>
        <div className="demo-otp-field-grouped-group">
          {Array.from({ length: 3 }, (_, index) => (
            <fs-otp-field-input key={index} aria-label={index === 0 ? undefined : `Character ${index + 1} of ${OTP_LENGTH}`} />
          ))}
        </div>
        <fs-otp-field-separator />
        <div className="demo-otp-field-grouped-group">
          {Array.from({ length: 3 }, (_, index) => (
            <fs-otp-field-input key={index + 3} aria-label={`Character ${index + 4} of ${OTP_LENGTH}`} />
          ))}
        </div>
      </fs-otp-field-root>
    </div>
  );
}
```

```css
/* stencil.css */
/* Example layout and visual exceptions. Default component styles come from the theme. */
@layer fs-theme {
  .demo-otp-field-grouped-field {
    display: flex;
    flex-direction: column;
    align-items: start;
    gap: var(--fs-space-1);
    width: 100%;
    max-width: 20rem;
  }

  .demo-otp-field-grouped-group {
    display: flex;
    gap: var(--fs-space-2);
  }
}
```

### Placeholder hints

`<OTPField.Input>` is a real input, so native `placeholder` props and CSS work as usual. This
example keeps placeholder hints visible until the active slot receives focus.

## Demo

### Tailwind

This example shows how to implement the component using Tailwind CSS.

```tsx
/* docs-comparison.tsx */
import { StencilComparison } from 'docs/src/components/StencilComparison';
import ReactOTPField from './react-otp-field';
import StencilOTPField from './stencil-otp-field';

export default function OTPFieldComparison() {
  return <StencilComparison key="tailwind" component="otp-field" react={<ReactOTPField />} stencil={<StencilOTPField />} />;
}
```

```tsx
/* react-otp-field.tsx */
import * as React from 'react';
import { OTPField } from '@base-ui/react/otp-field';

const CODE_LENGTH = 6;

export default function OTPFieldFocusedPlaceholderDemo() {
  const id = React.useId();
  const descriptionId = `${id}-description`;

  return (
    <div className="flex w-full max-w-80 flex-col items-start gap-1">
      <label htmlFor={id} className="text-sm font-bold text-neutral-950 dark:text-white">
        Verification code
      </label>
      <OTPField.Root id={id} length={CODE_LENGTH} aria-describedby={descriptionId} className="flex w-full gap-2">
        {Array.from({ length: CODE_LENGTH }, (_, index) => (
          <OTPField.Input
            key={index}
            className="m-0 h-10 w-10 rounded-none border border-neutral-950 bg-white dark:bg-neutral-950 text-center font-inherit text-base font-normal text-neutral-950 placeholder:text-neutral-500 focus:outline-2 focus:-outline-offset-1 focus:outline-neutral-950 dark:focus:outline-white focus:placeholder:text-transparent dark:border-white dark:text-white dark:placeholder:text-neutral-400"
            placeholder="•"
            aria-label={index === 0 ? undefined : `Character ${index + 1} of ${CODE_LENGTH}`}
          />
        ))}
      </OTPField.Root>
      <p id={descriptionId} className="m-0 text-sm text-neutral-600 dark:text-neutral-400">
        Placeholder hints can stay visible until the active slot is focused.
      </p>
    </div>
  );
}
```

```tsx
/* stencil-otp-field.tsx */
import * as React from 'react';

const CODE_LENGTH = 6;

export default function StencilOTPField() {
  const id = `stencil-focused-placeholder-${React.useId()}`;
  const descriptionId = `${id}-description`;

  return (
    <div className="flex w-full max-w-80 flex-col items-start gap-1">
      <label htmlFor={id} className="text-sm font-bold text-neutral-950 dark:text-white">
        Verification code
      </label>
      <fs-otp-field-root id={id} length={CODE_LENGTH} aria-describedby={descriptionId} className="flex w-full gap-2">
        {Array.from({ length: CODE_LENGTH }, (_, index) => (
          <fs-otp-field-input
            key={index}
            className="m-0 h-10 w-10 rounded-none border border-neutral-950 bg-white dark:bg-neutral-950 text-center font-inherit text-base font-normal text-neutral-950 placeholder:text-neutral-500 focus:outline-2 focus:-outline-offset-1 focus:outline-neutral-950 dark:focus:outline-white focus:placeholder:text-transparent dark:border-white dark:text-white dark:placeholder:text-neutral-400"
            placeholder="•"
            aria-label={index === 0 ? undefined : `Character ${index + 1} of ${CODE_LENGTH}`}
          />
        ))}
      </fs-otp-field-root>
      <p id={descriptionId} className="m-0 text-sm text-neutral-600 dark:text-neutral-400">
        Placeholder hints can stay visible until the active slot is focused.
      </p>
    </div>
  );
}
```

### CSS Modules

This example shows how to implement the component using CSS Modules.

```css
/* index.module.css */
.Field {
  display: flex;
  flex-direction: column;
  align-items: start;
  gap: 0.25rem;
  width: 100%;
  max-width: 20rem;
}

.Label {
  font-size: 0.875rem;
  line-height: 1.25rem;
  font-weight: 700;
  color: oklch(14.5% 0 0deg);

  @media (prefers-color-scheme: dark) {
    color: white;
  }
}

.Root {
  display: flex;
  gap: 0.5rem;
  width: 100%;
}

.Input {
  box-sizing: border-box;
  margin: 0;
  padding: 0;

  border: 1px solid oklch(14.5% 0 0deg);
  width: 2.5rem;
  height: 2.5rem;
  border-radius: 0;
  font-family: inherit;
  font-size: 1rem;
  line-height: 1.5rem;
  font-weight: 400;
  text-align: center;
  background-color: white;
  color: oklch(14.5% 0 0deg);

  @media (prefers-color-scheme: dark) {
    border: 1px solid white;
    background-color: oklch(14.5% 0 0deg);
    color: white;
  }

  &::placeholder {
    color: oklch(43.9% 0 0deg);

    @media (prefers-color-scheme: dark) {
      color: oklch(70.8% 0 0deg);
    }
  }

  &:focus {
    outline: 2px solid oklch(14.5% 0 0deg);
    outline-offset: -1px;

    @media (prefers-color-scheme: dark) {
      outline-color: white;
    }
  }

  &:focus::placeholder {
    color: transparent;
  }
}

.Description {
  margin: 0;
  font-size: 0.875rem;
  line-height: 1.25rem;
  color: oklch(43.9% 0 0deg);

  @media (prefers-color-scheme: dark) {
    color: oklch(70.8% 0 0deg);
  }
}
```

```tsx
/* docs-comparison.tsx */
import { StencilComparison } from 'docs/src/components/StencilComparison';
import ReactOTPField from './react-otp-field';
import StencilOTPField from './stencil-otp-field';

export default function OTPFieldComparison() {
  return <StencilComparison key="css-modules" component="otp-field" react={<ReactOTPField />} stencil={<StencilOTPField />} />;
}
```

```tsx
/* react-otp-field.tsx */
import * as React from 'react';
import { OTPField } from '@base-ui/react/otp-field';
import styles from './index.module.css';

const CODE_LENGTH = 6;

export default function OTPFieldFocusedPlaceholderDemo() {
  const id = React.useId();
  const descriptionId = `${id}-description`;

  return (
    <div className={styles.Field}>
      <label htmlFor={id} className={styles.Label}>
        Verification code
      </label>
      <OTPField.Root id={id} length={CODE_LENGTH} aria-describedby={descriptionId} className={styles.Root}>
        {Array.from({ length: CODE_LENGTH }, (_, index) => (
          <OTPField.Input key={index} className={styles.Input} placeholder="•" aria-label={index === 0 ? undefined : `Character ${index + 1} of ${CODE_LENGTH}`} />
        ))}
      </OTPField.Root>
      <p id={descriptionId} className={styles.Description}>
        Placeholder hints can stay visible until the active slot is focused.
      </p>
    </div>
  );
}
```

```tsx
/* stencil-otp-field.tsx */
import * as React from 'react';
import './stencil.css';

const CODE_LENGTH = 6;

export default function StencilOTPField() {
  const id = `stencil-focused-placeholder-${React.useId()}`;
  const descriptionId = `${id}-description`;

  return (
    <div className="demo-otp-field-focused-placeholder-field">
      <label htmlFor={id}>Verification code</label>
      <fs-otp-field-root id={id} length={CODE_LENGTH} aria-describedby={descriptionId}>
        {Array.from({ length: CODE_LENGTH }, (_, index) => (
          <fs-otp-field-input
            key={index}
            className="demo-otp-field-focused-placeholder-input"
            placeholder="•"
            aria-label={index === 0 ? undefined : `Character ${index + 1} of ${CODE_LENGTH}`}
          />
        ))}
      </fs-otp-field-root>
      <p id={descriptionId}>Placeholder hints can stay visible until the active slot is focused.</p>
    </div>
  );
}
```

```css
/* stencil.css */
/* Example layout and visual exceptions. Default component styles come from the theme. */
@layer fs-theme {
  .demo-otp-field-focused-placeholder-field {
    display: flex;
    flex-direction: column;
    align-items: start;
    gap: var(--fs-space-1);
    width: 100%;
    max-width: 20rem;
  }

  .demo-otp-field-focused-placeholder-input {
    &::placeholder {
      color: var(--fs-color-secondary);
    }

    &:focus::placeholder {
      color: var(--fs-palette-transparent);
    }
  }
}
```

### Custom normalization

Use `normalizeValue` to normalize accepted values before state updates, such as converting
alphanumeric codes to uppercase. It runs after `validationType` filtering, and the result is filtered
against `validationType` again. Use `validationType="none"` when the normalizer should provide the
full validation rule.

Pair custom rules with `inputMode` for keyboard hints and `onValueInvalid` for rejected characters.

## Demo

### CSS Modules

This example shows how to implement the component using CSS Modules.

```css
/* index.module.css */
.Field {
  display: flex;
  flex-direction: column;
  align-items: start;
  gap: 0.25rem;
  width: 100%;
  max-width: 20rem;
}

.Label {
  font-size: 0.875rem;
  line-height: 1.25rem;
  font-weight: 700;
  color: oklch(14.5% 0 0deg);

  @media (prefers-color-scheme: dark) {
    color: white;
  }
}

.Root {
  display: flex;
  gap: 0.5rem;
  width: 100%;
}

.Input {
  box-sizing: border-box;
  margin: 0;
  padding: 0;

  border: 1px solid oklch(14.5% 0 0deg);
  width: 2.5rem;
  height: 2.5rem;
  border-radius: 0;
  font-family: inherit;
  font-size: 1rem;
  line-height: 1.5rem;
  font-weight: 400;
  text-align: center;
  background-color: white;
  color: oklch(14.5% 0 0deg);

  @media (prefers-color-scheme: dark) {
    border: 1px solid white;
    background-color: oklch(14.5% 0 0deg);
    color: white;
  }

  &:focus {
    outline: 2px solid oklch(14.5% 0 0deg);
    outline-offset: -1px;

    @media (prefers-color-scheme: dark) {
      outline-color: white;
    }
  }
}

.InputInvalidA {
  border-color: oklch(50.5% 0.213 27.518deg);
  outline: 2px solid oklch(50.5% 0.213 27.518deg);
  outline-offset: -1px;
  animation: otp-field-shake-a 180ms ease-in-out;

  @media (prefers-color-scheme: dark) {
    border-color: oklch(70.4% 0.191 22.216deg);
    outline: 2px solid oklch(70.4% 0.191 22.216deg);
  }

  &:focus {
    outline: 2px solid oklch(50.5% 0.213 27.518deg);
    outline-offset: -1px;

    @media (prefers-color-scheme: dark) {
      outline-color: oklch(70.4% 0.191 22.216deg);
    }
  }
}

.InputInvalidB {
  border-color: oklch(50.5% 0.213 27.518deg);
  outline: 2px solid oklch(50.5% 0.213 27.518deg);
  outline-offset: -1px;
  animation: otp-field-shake-b 180ms ease-in-out;

  @media (prefers-color-scheme: dark) {
    border-color: oklch(70.4% 0.191 22.216deg);
    outline: 2px solid oklch(70.4% 0.191 22.216deg);
  }

  &:focus {
    outline: 2px solid oklch(50.5% 0.213 27.518deg);
    outline-offset: -1px;

    @media (prefers-color-scheme: dark) {
      outline-color: oklch(70.4% 0.191 22.216deg);
    }
  }
}

.Description {
  margin: 0;
  font-size: 0.875rem;
  line-height: 1.25rem;
  color: oklch(43.9% 0 0deg);

  @media (prefers-color-scheme: dark) {
    color: oklch(70.8% 0 0deg);
  }
}

.ScreenReaderOnly {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

@keyframes otp-field-shake-a {
  0%,
  100% {
    transform: translateX(0);
  }

  25% {
    transform: translateX(-4px);
  }

  75% {
    transform: translateX(4px);
  }
}

@keyframes otp-field-shake-b {
  0%,
  100% {
    transform: translateX(0);
  }

  25% {
    transform: translateX(-4px);
  }

  75% {
    transform: translateX(4px);
  }
}
```

```tsx
/* docs-comparison.tsx */
import { StencilComparison } from 'docs/src/components/StencilComparison';
import ReactOTPField from './react-otp-field';
import StencilOTPField from './stencil-otp-field';
import { StencilOTPFieldView } from './stencil-otp-field-view';

export default function OTPFieldComparison() {
  return <StencilComparison key="css-modules" component="otp-field" react={<ReactOTPField />} stencil={<StencilOTPFieldView />} clientStencil={<StencilOTPField />} />;
}
```

```tsx
/* react-otp-field.tsx */
'use client';
import * as React from 'react';
import { OTPField } from '@base-ui/react/otp-field';
import { useInvalidFeedback } from './useInvalidFeedback';
import styles from './index.module.css';

const CODE_LENGTH = 6;

function normalizeRecoveryCode(value: string) {
  return value.toUpperCase();
}

function getInvalidClassName(invalidPulse: number, evenClassName: string, oddClassName: string) {
  if (invalidPulse === 0) {
    return '';
  }

  return invalidPulse % 2 === 0 ? evenClassName : oddClassName;
}

export default function OTPFieldCustomNormalizeDemo() {
  const id = React.useId();
  const descriptionId = `${id}-description`;

  const { activeInvalidIndex, handleValueChange, handleValueInvalid, invalidPulse, setFocusedIndex, statusMessage } = useInvalidFeedback();

  const invalidClassName = getInvalidClassName(invalidPulse, styles.InputInvalidB, styles.InputInvalidA);

  return (
    <div className={styles.Field}>
      <label htmlFor={id} className={styles.Label}>
        Recovery code
      </label>
      <OTPField.Root
        id={id}
        length={CODE_LENGTH}
        validationType="alphanumeric"
        normalizeValue={normalizeRecoveryCode}
        onValueChange={handleValueChange}
        onValueInvalid={handleValueInvalid}
        aria-describedby={descriptionId}
        className={styles.Root}
      >
        {Array.from({ length: CODE_LENGTH }, (_, index) => (
          <OTPField.Input
            key={index}
            className={`${styles.Input} ${activeInvalidIndex === index ? invalidClassName : ''}`.trim()}
            aria-label={index === 0 ? undefined : `Character ${index + 1} of ${CODE_LENGTH}`}
            onFocus={() => {
              setFocusedIndex(index);
            }}
          />
        ))}
      </OTPField.Root>
      <p id={descriptionId} className={styles.Description}>
        Letters and digits only. Letters are converted to uppercase.
      </p>
      <span aria-live="polite" className={styles.ScreenReaderOnly}>
        {statusMessage}
      </span>
    </div>
  );
}
```

```tsx
/* stencil-otp-field.tsx */
'use client';
import { useInvalidFeedback } from './stencil-use-invalid-feedback';
import { StencilOTPFieldView } from './stencil-otp-field-view';

function normalizeRecoveryCode(value: string) {
  return value.toUpperCase();
}

export default function StencilOTPField() {
  const { activeInvalidIndex, handleValueChange, handleValueInvalid, invalidPulse, setFocusedIndex, statusMessage } = useInvalidFeedback();
  return (
    <StencilOTPFieldView
      activeInvalidIndex={activeInvalidIndex}
      invalidPulse={invalidPulse}
      statusMessage={statusMessage}
      normalizeValue={normalizeRecoveryCode}
      onValueChange={handleValueChange}
      onValueInvalid={event => handleValueInvalid(event.detail.value)}
      onSlotFocus={setFocusedIndex}
    />
  );
}
```

```tsx
/* stencil-otp-field-view.tsx */
import * as React from 'react';
import type { OTPFieldValueChangeDetail, OTPFieldValueInvalidDetail } from 'finesoft-components/define-custom-elements';
import './stencil.css';

const CODE_LENGTH = 6;

interface StencilOTPFieldViewProps {
  activeInvalidIndex?: number;
  invalidPulse?: number;
  statusMessage?: string;
  normalizeValue?: (value: string) => string;
  onValueChange?: (event: CustomEvent<OTPFieldValueChangeDetail>) => void;
  onValueInvalid?: (event: CustomEvent<OTPFieldValueInvalidDetail>) => void;
  onSlotFocus?: (index: number) => void;
}

export function StencilOTPFieldView({
  activeInvalidIndex = -1,
  invalidPulse = 0,
  statusMessage = '',
  normalizeValue,
  onValueChange,
  onValueInvalid,
  onSlotFocus,
}: StencilOTPFieldViewProps) {
  const id = `stencil-custom-normalize-${React.useId()}`;
  const descriptionId = `${id}-description`;
  const invalidClassName = invalidPulse === 0 ? '' : invalidPulse % 2 === 0 ? 'demo-otp-field-custom-sanitize-input-invalid-b' : 'demo-otp-field-custom-sanitize-input-invalid-a';

  return (
    <div className="demo-otp-field-custom-sanitize-field">
      <label htmlFor={id}>Recovery code</label>
      <fs-otp-field-root
        id={id}
        length={CODE_LENGTH}
        validation-type="alphanumeric"
        normalizeValue={normalizeValue}
        onvalueChange={onValueChange}
        onvalueInvalid={onValueInvalid}
        aria-describedby={descriptionId}
      >
        {Array.from({ length: CODE_LENGTH }, (_, index) => (
          <fs-otp-field-input
            key={index}
            className={activeInvalidIndex === index ? invalidClassName : undefined}
            aria-label={index === 0 ? undefined : `Character ${index + 1} of ${CODE_LENGTH}`}
            onFocus={() => onSlotFocus?.(index)}
          />
        ))}
      </fs-otp-field-root>
      <p id={descriptionId}>Letters and digits only. Letters are converted to uppercase.</p>
      <span aria-live="polite" className="demo-otp-field-custom-sanitize-screen-reader-only">
        {statusMessage}
      </span>
    </div>
  );
}
```

```ts
/* useInvalidFeedback.ts */
'use client';
import * as React from 'react';

export function useInvalidFeedback() {
  const [focusedIndex, setFocusedIndex] = React.useState(0);
  const [invalidPulse, setInvalidPulse] = React.useState(0);
  const [statusMessage, setStatusMessage] = React.useState('');
  const invalidTimeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
  const skipClearOnNextValueChangeRef = React.useRef(false);

  React.useEffect(() => {
    return () => {
      if (invalidTimeoutRef.current != null) {
        clearTimeout(invalidTimeoutRef.current);
      }
    };
  }, []);

  function clearInvalidFeedback() {
    if (invalidTimeoutRef.current != null) {
      clearTimeout(invalidTimeoutRef.current);
      invalidTimeoutRef.current = null;
    }

    setInvalidPulse(0);
    setStatusMessage('');
  }

  function handleValueChange() {
    if (skipClearOnNextValueChangeRef.current) {
      skipClearOnNextValueChangeRef.current = false;
      return;
    }

    clearInvalidFeedback();
  }

  function handleValueInvalid(value: string) {
    skipClearOnNextValueChangeRef.current = true;
    setInvalidPulse(current => current + 1);
    setStatusMessage(`Unsupported characters were ignored from ${value}.`);

    if (invalidTimeoutRef.current != null) {
      clearTimeout(invalidTimeoutRef.current);
    }

    invalidTimeoutRef.current = setTimeout(() => {
      invalidTimeoutRef.current = null;
      setInvalidPulse(0);
    }, 400);
  }

  return {
    activeInvalidIndex: invalidPulse > 0 ? focusedIndex : -1,
    invalidPulse,
    statusMessage,
    setFocusedIndex,
    handleValueChange,
    handleValueInvalid,
  };
}
```

```ts
/* stencil-use-invalid-feedback.ts */
'use client';
import * as React from 'react';

export function useInvalidFeedback() {
  const [focusedIndex, setFocusedIndex] = React.useState(0);
  const [invalidPulse, setInvalidPulse] = React.useState(0);
  const [statusMessage, setStatusMessage] = React.useState('');
  const invalidTimeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
  const skipClearOnNextValueChangeRef = React.useRef(false);

  React.useEffect(() => {
    return () => {
      if (invalidTimeoutRef.current != null) {
        clearTimeout(invalidTimeoutRef.current);
      }
    };
  }, []);

  function clearInvalidFeedback() {
    if (invalidTimeoutRef.current != null) {
      clearTimeout(invalidTimeoutRef.current);
      invalidTimeoutRef.current = null;
    }

    setInvalidPulse(0);
    setStatusMessage('');
  }

  function handleValueChange() {
    if (skipClearOnNextValueChangeRef.current) {
      skipClearOnNextValueChangeRef.current = false;
      return;
    }

    clearInvalidFeedback();
  }

  function handleValueInvalid(value: string) {
    skipClearOnNextValueChangeRef.current = true;
    setInvalidPulse(current => current + 1);
    setStatusMessage(`Unsupported characters were ignored from ${value}.`);

    if (invalidTimeoutRef.current != null) {
      clearTimeout(invalidTimeoutRef.current);
    }

    invalidTimeoutRef.current = setTimeout(() => {
      invalidTimeoutRef.current = null;
      setInvalidPulse(0);
    }, 400);
  }

  return {
    activeInvalidIndex: invalidPulse > 0 ? focusedIndex : -1,
    invalidPulse,
    statusMessage,
    setFocusedIndex,
    handleValueChange,
    handleValueInvalid,
  };
}
```

```css
/* stencil.css */
/* Example layout and visual exceptions. Default component styles come from the theme. */
@layer fs-theme {
  .demo-otp-field-custom-sanitize-field {
    display: flex;
    flex-direction: column;
    align-items: start;
    gap: var(--fs-space-1);
    width: 100%;
    max-width: 20rem;
  }

  .demo-otp-field-custom-sanitize-input-invalid-a {
    border-color: var(--fs-color-danger);
    outline: var(--fs-focus-width) solid var(--fs-color-danger);
    outline-offset: -1px;
    animation: fs-otp-field-custom-sanitize-otp-field-shake-a 180ms ease-in-out;

    &:focus {
      outline: var(--fs-focus-width) solid var(--fs-color-danger);
      outline-offset: -1px;
    }
  }

  .demo-otp-field-custom-sanitize-input-invalid-b {
    border-color: var(--fs-color-danger);
    outline: var(--fs-focus-width) solid var(--fs-color-danger);
    outline-offset: -1px;
    animation: fs-otp-field-custom-sanitize-otp-field-shake-b 180ms ease-in-out;

    &:focus {
      outline: var(--fs-focus-width) solid var(--fs-color-danger);
      outline-offset: -1px;
    }
  }

  .demo-otp-field-custom-sanitize-screen-reader-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
  }

  @keyframes fs-otp-field-custom-sanitize-otp-field-shake-a {
    0%,
    100% {
      transform: translateX(0);
    }

    25% {
      transform: translateX(-4px);
    }

    75% {
      transform: translateX(4px);
    }
  }

  @keyframes fs-otp-field-custom-sanitize-otp-field-shake-b {
    0%,
    100% {
      transform: translateX(0);
    }

    25% {
      transform: translateX(-4px);
    }

    75% {
      transform: translateX(4px);
    }
  }
}
```

### Masked entry

Use `mask` when the code should be obscured while it is being typed.

## Demo

### Tailwind

This example shows how to implement the component using Tailwind CSS.

```tsx
/* docs-comparison.tsx */
import { StencilComparison } from 'docs/src/components/StencilComparison';
import ReactOTPField from './react-otp-field';
import StencilOTPField from './stencil-otp-field';

export default function OTPFieldComparison() {
  return <StencilComparison key="tailwind" component="otp-field" react={<ReactOTPField />} stencil={<StencilOTPField />} />;
}
```

```tsx
/* react-otp-field.tsx */
import * as React from 'react';
import { OTPField } from '@base-ui/react/otp-field';

const CODE_LENGTH = 6;

export default function OTPFieldPasswordDemo() {
  const id = React.useId();
  const descriptionId = `${id}-description`;

  return (
    <div className="flex w-full max-w-80 flex-col items-start gap-1">
      <label htmlFor={id} className="text-sm font-bold text-neutral-950 dark:text-white">
        Access code
      </label>
      <OTPField.Root id={id} length={CODE_LENGTH} mask aria-describedby={descriptionId} className="flex w-full gap-2">
        {Array.from({ length: CODE_LENGTH }, (_, index) => (
          <OTPField.Input
            key={index}
            className="m-0 h-10 w-10 rounded-none border border-neutral-950 bg-white dark:bg-neutral-950 text-center font-inherit text-base font-normal text-neutral-950 focus:outline-2 focus:-outline-offset-1 focus:outline-neutral-950 dark:focus:outline-white dark:border-white dark:text-white"
            aria-label={index === 0 ? undefined : `Character ${index + 1} of ${CODE_LENGTH}`}
          />
        ))}
      </OTPField.Root>
      <p id={descriptionId} className="m-0 text-sm text-neutral-600 dark:text-neutral-400">
        Use <code className="font-mono">mask</code> to obscure the code on shared screens.
      </p>
    </div>
  );
}
```

```tsx
/* stencil-otp-field.tsx */
import * as React from 'react';

const CODE_LENGTH = 6;

export default function StencilOTPField() {
  const id = `stencil-password-${React.useId()}`;
  const descriptionId = `${id}-description`;

  return (
    <div className="flex w-full max-w-80 flex-col items-start gap-1">
      <label htmlFor={id} className="text-sm font-bold text-neutral-950 dark:text-white">
        Access code
      </label>
      <fs-otp-field-root id={id} length={CODE_LENGTH} mask aria-describedby={descriptionId} className="flex w-full gap-2">
        {Array.from({ length: CODE_LENGTH }, (_, index) => (
          <fs-otp-field-input
            key={index}
            className="m-0 h-10 w-10 rounded-none border border-neutral-950 bg-white dark:bg-neutral-950 text-center font-inherit text-base font-normal text-neutral-950 focus:outline-2 focus:-outline-offset-1 focus:outline-neutral-950 dark:focus:outline-white dark:border-white dark:text-white"
            aria-label={index === 0 ? undefined : `Character ${index + 1} of ${CODE_LENGTH}`}
          />
        ))}
      </fs-otp-field-root>
      <p id={descriptionId} className="m-0 text-sm text-neutral-600 dark:text-neutral-400">
        Use <code className="font-mono">mask</code> to obscure the code on shared screens.
      </p>
    </div>
  );
}
```

### CSS Modules

This example shows how to implement the component using CSS Modules.

```css
/* index.module.css */
.Field {
  display: flex;
  flex-direction: column;
  align-items: start;
  gap: 0.25rem;
  width: 100%;
  max-width: 20rem;
}

.Label {
  font-size: 0.875rem;
  line-height: 1.25rem;
  font-weight: 700;
  color: oklch(14.5% 0 0deg);

  @media (prefers-color-scheme: dark) {
    color: white;
  }
}

.Root {
  display: flex;
  gap: 0.5rem;
  width: 100%;
}

.Input {
  box-sizing: border-box;
  margin: 0;
  padding: 0;

  border: 1px solid oklch(14.5% 0 0deg);
  width: 2.5rem;
  height: 2.5rem;
  border-radius: 0;
  font-family: inherit;
  font-size: 1rem;
  line-height: 1.5rem;
  font-weight: 400;
  text-align: center;
  background-color: white;
  color: oklch(14.5% 0 0deg);

  @media (prefers-color-scheme: dark) {
    border: 1px solid white;
    background-color: oklch(14.5% 0 0deg);
    color: white;
  }

  &:focus {
    outline: 2px solid oklch(14.5% 0 0deg);
    outline-offset: -1px;

    @media (prefers-color-scheme: dark) {
      outline-color: white;
    }
  }
}

.Description {
  margin: 0;
  font-size: 0.875rem;
  line-height: 1.25rem;
  color: oklch(43.9% 0 0deg);

  @media (prefers-color-scheme: dark) {
    color: oklch(70.8% 0 0deg);
  }
}

.Code {
  font-family: 'Paper Mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
}
```

```tsx
/* docs-comparison.tsx */
import { StencilComparison } from 'docs/src/components/StencilComparison';
import ReactOTPField from './react-otp-field';
import StencilOTPField from './stencil-otp-field';

export default function OTPFieldComparison() {
  return <StencilComparison key="css-modules" component="otp-field" react={<ReactOTPField />} stencil={<StencilOTPField />} />;
}
```

```tsx
/* react-otp-field.tsx */
import * as React from 'react';
import { OTPField } from '@base-ui/react/otp-field';
import styles from './index.module.css';

const CODE_LENGTH = 6;

export default function OTPFieldPasswordDemo() {
  const id = React.useId();
  const descriptionId = `${id}-description`;

  return (
    <div className={styles.Field}>
      <label htmlFor={id} className={styles.Label}>
        Access code
      </label>
      <OTPField.Root id={id} length={CODE_LENGTH} mask aria-describedby={descriptionId} className={styles.Root}>
        {Array.from({ length: CODE_LENGTH }, (_, index) => (
          <OTPField.Input key={index} className={styles.Input} aria-label={index === 0 ? undefined : `Character ${index + 1} of ${CODE_LENGTH}`} />
        ))}
      </OTPField.Root>
      <p id={descriptionId} className={styles.Description}>
        Use <span className={styles.Code}>mask</span> to obscure the code on shared screens.
      </p>
    </div>
  );
}
```

```tsx
/* stencil-otp-field.tsx */
import * as React from 'react';
import './stencil.css';

const CODE_LENGTH = 6;

export default function StencilOTPField() {
  const id = `stencil-password-${React.useId()}`;
  const descriptionId = `${id}-description`;

  return (
    <div className="demo-otp-field-password-field">
      <label htmlFor={id}>Access code</label>
      <fs-otp-field-root id={id} length={CODE_LENGTH} mask aria-describedby={descriptionId}>
        {Array.from({ length: CODE_LENGTH }, (_, index) => (
          <fs-otp-field-input key={index} aria-label={index === 0 ? undefined : `Character ${index + 1} of ${CODE_LENGTH}`} />
        ))}
      </fs-otp-field-root>
      <p id={descriptionId}>
        Use <span className="demo-otp-field-password-code">mask</span> to obscure the code on shared screens.
      </p>
    </div>
  );
}
```

```css
/* stencil.css */
/* Example layout and visual exceptions. Default component styles come from the theme. */
@layer fs-theme {
  .demo-otp-field-password-field {
    display: flex;
    flex-direction: column;
    align-items: start;
    gap: var(--fs-space-1);
    width: 100%;
    max-width: 20rem;
  }

  .demo-otp-field-password-code {
    font-family: 'Paper Mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
  }
}
```

## API reference

### Root

Groups all OTP field parts and manages their state.
Renders a `<div>` element.

**Root Props:**

| Prop            | Type                                                                                        | Default           | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| :-------------- | :------------------------------------------------------------------------------------------ | :---------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| name            | `string`                                                                                    | -                 | Identifies the field when a form is submitted.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| defaultValue    | `string`                                                                                    | -                 | The uncontrolled OTP value when the component is initially rendered.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| value           | `string`                                                                                    | -                 | The OTP value.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| onValueChange   | `((value: string, eventDetails: OTPField.Root.ChangeEventDetails) => void)`                 | -                 | Callback fired when the OTP value changes. The `eventDetails.reason` indicates what triggered the change: `'input-change'` for typing or autofill`'input-clear'` when a character is removed by text input`'input-paste'` for paste interactions`'keyboard'` for keyboard interactions that change the value                                                                                                                                                                                                                                                                                                                                                                     |
| autoComplete    | `string`                                                                                    | `'one-time-code'` | The input autocomplete attribute. Applied to the first slot and hidden validation input.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| autoSubmit      | `boolean`                                                                                   | `false`           | Whether to submit the owning form when the OTP becomes complete.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| form            | `string`                                                                                    | -                 | A string specifying the `form` element with which the hidden input is associated.&#xA;This string's value must match the id of a `form` element in the same document.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| inputMode       | `'none' \| 'text' \| 'tel' \| 'url' \| 'email' \| 'numeric' \| 'decimal' \| 'search'`       | -                 | The virtual keyboard hint applied to the slot inputs and hidden validation input. Built-in validation modes provide sensible defaults, but you can override them when needed.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| length\*        | `number`                                                                                    | -                 | The number of OTP input slots.&#xA;Required so the root can clamp values, detect completion, and generate&#xA;consistent validation markup before all slots hydrate.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| mask            | `boolean`                                                                                   | `false`           | Whether the slot inputs should mask entered characters.&#xA;Pass `type` directly to individual `<OTPField.Input>` parts to use a custom&#xA;input type.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| normalizeValue  | `((value: string) => string)`                                                               | -                 | Function that normalizes the OTP value after whitespace and `validationType` filtering.&#xA;It runs whenever OTP Field normalizes a value, including initial/default values, controlled&#xA;values, and user edits. The returned value is filtered by `validationType` again, then clamped to `length`.&#xA;It should be idempotent because OTP Field may normalize the same value more than once while&#xA;handling edits, storing state, and rendering controlled or uncontrolled values. Non-idempotent&#xA;normalizers can compound across those normalization passes. Characters rejected while&#xA;normalizing typed or pasted text are reported through `onValueInvalid`. |
| onValueComplete | `((value: string, eventDetails: OTPField.Root.CompleteEventDetails) => void)`               | -                 | Callback function that is fired when the OTP value becomes complete, or when a complete value&#xA;is pasted while the OTP is already complete. When the value changes, it runs later than `onValueChange`, after the internal value update is&#xA;applied. If a complete pasted value matches the current value, `onValueChange` does not fire. If `autoSubmit` is enabled, it runs immediately before the owning form is submitted.                                                                                                                                                                                                                                             |
| onValueInvalid  | `((value: string, eventDetails: OTPField.Root.InvalidEventDetails) => void)`                | -                 | Callback fired when entered text contains characters that are rejected by validation or&#xA;normalization before the OTP value updates. The `value` argument is the attempted user-entered string before normalization.                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| validationType  | `OTPField.Root.ValidationType`                                                              | `'numeric'`       | The type of input validation to apply to the OTP value.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| disabled        | `boolean`                                                                                   | `false`           | Whether the component should ignore user interaction.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| readOnly        | `boolean`                                                                                   | `false`           | Whether the user should be unable to change the field value.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| required        | `boolean`                                                                                   | `false`           | Whether the user must enter a value before submitting a form.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| id              | `string`                                                                                    | -                 | The id of the first input element.&#xA;Subsequent inputs derive their ids from it (`{id}-2`, `{id}-3`, and so on).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| className       | `string \| ((state: OTPField.Root.State) => string \| undefined)`                           | -                 | CSS class applied to the element, or a function that&#xA;returns a class based on the component's state.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| style           | `React.CSSProperties \| ((state: OTPField.Root.State) => React.CSSProperties \| undefined)` | -                 | Style applied to the element, or a function that&#xA;returns a style object based on the component's state.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| render          | `ReactElement \| ((props: HTMLProps, state: OTPField.Root.State) => ReactElement)`          | -                 | Allows you to replace the component's HTML element&#xA;with a different tag, or compose it with another component. Accepts a `ReactElement` or a function that returns the element to render.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |

**Root Data Attributes:**

| Attribute     | Type | Description                                                                     |
| :------------ | :--- | :------------------------------------------------------------------------------ |
| data-disabled | -    | Present when the OTP field is disabled.                                         |
| data-readonly | -    | Present when the OTP field is readonly.                                         |
| data-required | -    | Present when the OTP field is required.                                         |
| data-valid    | -    | Present when the OTP field is in a valid state (when wrapped in Field.Root).    |
| data-invalid  | -    | Present when the OTP field is in an invalid state (when wrapped in Field.Root). |
| data-dirty    | -    | Present when the OTP field's value has changed (when wrapped in Field.Root).    |
| data-touched  | -    | Present when the OTP field has been touched (when wrapped in Field.Root).       |
| data-complete | -    | Present when all slots are filled.                                              |
| data-filled   | -    | Present when the OTP field contains at least one character.                     |
| data-focused  | -    | Present when one of the OTP field inputs is focused.                            |

### Root.Props

Re-export of [Root](/react/components/otp-field.md) props.

### Root.State

```typescript
type OTPFieldRootState = {
  /** Whether all slots are filled. */
  complete: boolean;
  /** Whether the component should ignore user interaction. */
  disabled: boolean;
  /** The number of OTP input slots. */
  length: number;
  /** Whether the user should be unable to change the field value. */
  readOnly: boolean;
  /** Whether the user must enter a value before submitting a form. */
  required: boolean;
  /** The OTP value. */
  value: string;
  /** Whether the field has been touched. */
  touched: boolean;
  /** Whether the field value has changed from its initial value. */
  dirty: boolean;
  /** Whether the field is valid. */
  valid: boolean | null;
  /** Whether the field has a value. */
  filled: boolean;
  /** Whether the field is focused. */
  focused: boolean;
};
```

### Root.ChangeEventReason

```typescript
type OTPFieldRootChangeEventReason = 'input-change' | 'input-clear' | 'input-paste' | 'keyboard';
```

### Root.ChangeEventDetails

```typescript
type OTPFieldRootChangeEventDetails = (
  | { reason: 'input-change'; event: InputEvent | Event }
  | { reason: 'input-clear'; event: InputEvent | Event | FocusEvent }
  | { reason: 'input-paste'; event: ClipboardEvent }
  | { reason: 'keyboard'; event: KeyboardEvent }
) & {
  /** Cancels Base UI from handling the event. */
  cancel: () => void;
  /** Allows the event to propagate in cases where Base UI will stop the propagation. */
  allowPropagation: () => void;
  /** Indicates whether the event has been canceled. */
  isCanceled: boolean;
  /** Indicates whether the event is allowed to propagate. */
  isPropagationAllowed: boolean;
  /** The element that triggered the event, if applicable. */
  trigger: Element | undefined;
};
```

### Root.CompleteEventDetails

```typescript
type OTPFieldRootCompleteEventDetails = { reason: 'input-change'; event: InputEvent | Event } | { reason: 'input-paste'; event: ClipboardEvent };
```

### Root.CompleteEventReason

```typescript
type OTPFieldRootCompleteEventReason = 'input-change' | 'input-paste';
```

### Root.InvalidEventDetails

```typescript
type OTPFieldRootInvalidEventDetails = { reason: 'input-change'; event: InputEvent | Event } | { reason: 'input-paste'; event: ClipboardEvent };
```

### Root.InvalidEventReason

```typescript
type OTPFieldRootInvalidEventReason = 'input-change' | 'input-paste';
```

### Root.ValidationType

```typescript
type OTPFieldRootValidationType = 'numeric' | 'alpha' | 'alphanumeric' | 'none';
```

### Input

An individual OTP character input.
Renders an `<input>` element.

**Input Props:**

| Prop      | Type                                                                                                                                                             | Default | Description                                                                                                                                                                                   |
| :-------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| className | `string \| ((state: OTPField.Input.State) => string \| undefined)`                                                                                               | -       | CSS class applied to the element, or a function that&#xA;returns a class based on the component's state.                                                                                      |
| style     | `React.CSSProperties \| ((state: OTPField.Input.State) => React.CSSProperties \| undefined)`                                                                     | -       | Style applied to the element, or a function that&#xA;returns a style object based on the component's state.                                                                                   |
| render    | `ReactElement \| ((props: React.DetailedHTMLProps<React.InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>, state: OTPField.Input.State) => ReactElement)` | -       | Allows you to replace the component's HTML element&#xA;with a different tag, or compose it with another component. Accepts a `ReactElement` or a function that returns the element to render. |

**Input Data Attributes:**

| Attribute     | Type | Description                                                                     |
| :------------ | :--- | :------------------------------------------------------------------------------ |
| data-disabled | -    | Present when the OTP field is disabled.                                         |
| data-readonly | -    | Present when the OTP field is readonly.                                         |
| data-required | -    | Present when the OTP field is required.                                         |
| data-valid    | -    | Present when the OTP field is in a valid state (when wrapped in Field.Root).    |
| data-invalid  | -    | Present when the OTP field is in an invalid state (when wrapped in Field.Root). |
| data-dirty    | -    | Present when the OTP field's value has changed (when wrapped in Field.Root).    |
| data-touched  | -    | Present when the OTP field has been touched (when wrapped in Field.Root).       |
| data-complete | -    | Present when all slots are filled.                                              |
| data-filled   | -    | Present when the input contains a character.                                    |
| data-focused  | -    | Present when any OTP field input is focused.                                    |

### Input.Props

Re-export of [Input](/react/components/otp-field.md) props.

### Input.State

```typescript
type OTPFieldInputState = {
  /** Whether this input contains a character. */
  filled: boolean;
  /** The input index. */
  index: number;
  /** The character rendered in this slot. */
  value: string;
  /** Whether the component should ignore user interaction. */
  disabled: boolean;
  /** The number of OTP input slots. */
  length: number;
  /** Whether the user must enter a value before submitting a form. */
  required: boolean;
  /** Whether the user should be unable to change the field value. */
  readOnly: boolean;
  /** Whether all slots are filled. */
  complete: boolean;
  /** Whether the field has been touched. */
  touched: boolean;
  /** Whether the field value has changed from its initial value. */
  dirty: boolean;
  /** Whether the field is valid. */
  valid: boolean | null;
  /** Whether the field is focused. */
  focused: boolean;
};
```

### Separator

A separator element accessible to screen readers.
Renders a `<div>` element.

**Separator Props:**

| Prop        | Type                                                                                   | Default        | Description                                                                                                                                                                                   |
| :---------- | :------------------------------------------------------------------------------------- | :------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| orientation | `Orientation`                                                                          | `'horizontal'` | The orientation of the separator.                                                                                                                                                             |
| className   | `string \| ((state: SeparatorState) => string \| undefined)`                           | -              | CSS class applied to the element, or a function that&#xA;returns a class based on the component's state.                                                                                      |
| style       | `React.CSSProperties \| ((state: SeparatorState) => React.CSSProperties \| undefined)` | -              | Style applied to the element, or a function that&#xA;returns a style object based on the component's state.                                                                                   |
| render      | `ReactElement \| ((props: HTMLProps, state: SeparatorState) => ReactElement)`          | -              | Allows you to replace the component's HTML element&#xA;with a different tag, or compose it with another component. Accepts a `ReactElement` or a function that returns the element to render. |

### Separator.Props

Re-export of [Separator](/react/components/otp-field.md) props.

### Separator.State

```typescript
type OTPFieldSeparatorState = {
  /** The orientation of the separator. */
  orientation: Orientation;
};
```

## External Types

### ValidationType

```typescript
type ValidationType = 'numeric' | 'alpha' | 'alphanumeric' | 'none';
```

### Orientation

```typescript
type Orientation = 'horizontal' | 'vertical';
```

## Export Groups

- `OTPField.Root`: `OTPField.Root`, `OTPField.Root.State`, `OTPField.Root.Props`, `OTPField.Root.ValidationType`, `OTPField.Root.ChangeEventReason`, `OTPField.Root.ChangeEventDetails`, `OTPField.Root.InvalidEventReason`, `OTPField.Root.InvalidEventDetails`, `OTPField.Root.CompleteEventReason`, `OTPField.Root.CompleteEventDetails`
- `OTPField.Input`: `OTPField.Input`, `OTPField.Input.State`, `OTPField.Input.Props`
- `OTPField.Separator`: `OTPField.Separator`, `OTPField.Separator.Props`, `OTPField.Separator.State`
- `Default`: `OTPFieldRootProps`, `OTPFieldRootState`, `OTPFieldRootChangeEventReason`, `OTPFieldRootChangeEventDetails`, `OTPFieldRootInvalidEventReason`, `OTPFieldRootInvalidEventDetails`, `OTPFieldRootCompleteEventReason`, `OTPFieldRootCompleteEventDetails`, `OTPFieldInputState`, `OTPFieldInputProps`

## Canonical Types

Maps `Canonical`: `Alias` — Use Canonical when its namespace is already imported; otherwise use Alias.

- `OTPField.Root.State`: `OTPFieldRootState`
- `OTPField.Root.Props`: `OTPFieldRootProps`
- `OTPField.Root.ChangeEventReason`: `OTPFieldRootChangeEventReason`
- `OTPField.Root.ChangeEventDetails`: `OTPFieldRootChangeEventDetails`
- `OTPField.Root.InvalidEventReason`: `OTPFieldRootInvalidEventReason`
- `OTPField.Root.InvalidEventDetails`: `OTPFieldRootInvalidEventDetails`
- `OTPField.Root.CompleteEventReason`: `OTPFieldRootCompleteEventReason`
- `OTPField.Root.CompleteEventDetails`: `OTPFieldRootCompleteEventDetails`
- `OTPField.Input.State`: `OTPFieldInputState`
- `OTPField.Input.Props`: `OTPFieldInputProps`
