# OTP field

A one-time code entered one character per slot. Supports paste and one-time-code autofill. For longer values, such as API keys, use [Input](/components/input.md).

```tsx
// demo.tsx
import {Field, OTPField} from '@earendil-works/elanor-react';

const LENGTH = 6;

export default function OTPFieldDemo() {
  return (
    <Field.Root name="code">
      <Field.Label>Verification code</Field.Label>
      <OTPField.Root length={LENGTH}>
        {Array.from({length: LENGTH}, (_, index) => (
          <OTPField.Input
            key={index}
            aria-label={index === 0 ? undefined : `Character ${index + 1} of ${LENGTH}`}
          />
        ))}
      </OTPField.Root>
    </Field.Root>
  );
}
```

## Usage

```tsx
import {Field, OTPField} from '@earendil-works/elanor-react';

<Field.Root name="code">
  <Field.Label>Verification code</Field.Label>
  <OTPField.Root length={6}>
    <OTPField.Input />
    <OTPField.Input aria-label="Character 2 of 6" />
    {/* … one Input per character */}
  </OTPField.Root>
</Field.Root>;
```

`Field.Label` labels the first slot; give the others an `aria-label`. The field submits one value under the `Field.Root`'s `name`.

## Validation

`validationType` defaults to `numeric` and sets a native pattern, so an incomplete code fails with `patternMismatch`. Give it a `Field.Error`, otherwise the browser's message shows.

```tsx
// validation.tsx
import {Button, Field, Form, OTPField, Stack} from '@earendil-works/elanor-react';

const LENGTH = 6;

export default function OTPFieldValidation() {
  return (
    <Form>
      <Stack gap="medium">
        <Field.Root name="code">
          <Field.Label>Verification code</Field.Label>
          <OTPField.Root length={LENGTH} required>
            {Array.from({length: LENGTH}, (_, index) => (
              <OTPField.Input
                key={index}
                aria-label={index === 0 ? undefined : `Character ${index + 1} of ${LENGTH}`}
              />
            ))}
          </OTPField.Root>
          <Field.Error match="valueMissing">Enter the code from the email.</Field.Error>
          <Field.Error match="patternMismatch">Enter all 6 digits.</Field.Error>
        </Field.Root>
        <Button type="submit">Verify</Button>
      </Stack>
    </Form>
  );
}
```

## Grouped

```tsx
// grouped.tsx
import {Field, OTPField} from '@earendil-works/elanor-react';

const LENGTH = 6;

function slot(index: number) {
  return (
    <OTPField.Input
      key={index}
      aria-label={index === 0 ? undefined : `Character ${index + 1} of ${LENGTH}`}
    />
  );
}

export default function OTPFieldGrouped() {
  return (
    <Field.Root name="code">
      <Field.Label>Verification code</Field.Label>
      <OTPField.Root length={LENGTH}>
        {[0, 1, 2].map(slot)}
        <OTPField.Separator />
        {[3, 4, 5].map(slot)}
      </OTPField.Root>
    </Field.Root>
  );
}
```

## Alphanumeric

Use `validationType="alphanumeric"` for recovery and invite codes. `normalizeValue` runs on every edit, so it must return the same value when run twice; define it outside the component.

```tsx
// alphanumeric.tsx
import {Field, OTPField} from '@earendil-works/elanor-react';

const LENGTH = 8;

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

export default function OTPFieldAlphanumeric() {
  return (
    <Field.Root name="recoveryCode">
      <Field.Label>Recovery code</Field.Label>
      <OTPField.Root length={LENGTH} validationType="alphanumeric" normalizeValue={uppercase}>
        {Array.from({length: LENGTH}, (_, index) => (
          <OTPField.Input
            key={index}
            aria-label={index === 0 ? undefined : `Character ${index + 1} of ${LENGTH}`}
          />
        ))}
      </OTPField.Root>
    </Field.Root>
  );
}
```

## Full width

Slots are square. Override their size with a class to fill the container.

```tsx
// full-width.tsx
import {Field, OTPField} from '@earendil-works/elanor-react';

import styles from './otp-field.module.css';

const LENGTH = 6;

export default function OTPFieldFullWidth() {
  return (
    <Field.Root name="code">
      <Field.Label>Verification code</Field.Label>
      <OTPField.Root length={LENGTH}>
        {Array.from({length: LENGTH}, (_, index) => (
          <OTPField.Input
            key={index}
            className={styles.stretch}
            aria-label={index === 0 ? undefined : `Character ${index + 1} of ${LENGTH}`}
          />
        ))}
      </OTPField.Root>
    </Field.Root>
  );
}
```

```css
/* otp-field.module.css */
.stretch {
  flex: 1 1 0;
  inline-size: auto;
}
```

## API

### `OTPField.Root`

Groups the slots and owns the value. Render one `Input` per character of `length`.

Accepts Base UI [`OTPField.Root`](https://base-ui.com/react/components/otp-field.md) props.

### `OTPField.Input`

One character slot. Label every slot after the first, for example "Character 2 of 6".

Accepts Base UI [`OTPField.Input`](https://base-ui.com/react/components/otp-field.md) props.

### `OTPField.Separator`

Splits the slots into groups, such as 123–456.

Accepts Base UI [`Separator`](https://base-ui.com/react/components/separator.md) props.
