# OTP Field (/docs/otp-field)





## API Reference [#api-reference]

<BaseUIReference href="https://base-ui.com/react/components/otp-field" />

## Basic [#basic]

`OTPField` renders input slots automatically from `length`. Use `inputProps` to customize every
automatic slot, `groupSize` for common grouped layouts, or manual children only when the layout
needs arbitrary per-slot markup.

<Preview cssProperties="otpFieldPlaygroundCssProperties">
  <OTPFieldExample />

  <Preview.Code>
    {`
          import { Field, FieldLabel, OTPField } from "moduix";
          import { useId } from "react";

          export function OTPFieldDemo() {
            const id = useId();

            return (
              <Field>
                <FieldLabel htmlFor={id}>Verification code</FieldLabel>
                <OTPField id={id} length={6} />
              </Field>
            );
          }
        `}
  </Preview.Code>

  <Preview.CSSProperties>
    {(context) => <OTPFieldCssPropertiesPanel {...context} />}
  </Preview.CSSProperties>

  <Preview.CSSPlayground>
    {(context) => <OTPFieldCssPlaygroundPanel {...context} />}
  </Preview.CSSPlayground>
</Preview>

## Anatomy [#anatomy]

`OTPField` owns the field state and renders one input slot for each character in `length`. The
automatic slots are the recommended default because labels, slot count, and basic accessibility
labels stay in one place.

```text
OTPField[length]
├─ OTPFieldInput
├─ OTPFieldInput
├─ OTPFieldSeparator (optional, from groupSize)
└─ OTPFieldInput
```

```tsx
<OTPField id={id} length={6} groupSize={3} />
```

| Part                | Role                                                                                                   |
| ------------------- | ------------------------------------------------------------------------------------------------------ |
| `OTPField`          | Root state machine. Controls value, validation, completion callbacks, masking, and automatic slots.    |
| `OTPFieldInput`     | Visible character slot. Rendered automatically unless custom `children` are provided.                  |
| `OTPFieldSeparator` | Optional visible separator between groups. Rendered automatically from `groupSize` or manually placed. |

`OTPField` does not use portal-like service layers such as `portal`, `backdrop`, `positioner`, or
`viewport`. In most cases, keep automatic input rendering and style the root or generated inputs
with `className`, `inputProps`, and CSS variables.

## Composition [#composition]

Use `length` for the number of slots. Add `groupSize` when the code should be visually split, for
example `groupSize={3}` for `123-456` or `groupSize={[3, 2]}` for `ABC-DE-F`.

`inputProps` accepts either an object for every generated slot or a function that receives
`index` and `length` for per-slot props. Use `separator` to replace the default separator icon when
`groupSize` is set. For layouts that need arbitrary markup around slots, pass `OTPFieldInput` and
`OTPFieldSeparator` as children manually.

## Examples [#examples]

### Alphanumeric [#alphanumeric]

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

<Preview>
  <OTPFieldAlphanumericExample />

  <Preview.Code>
    {`
          import {
            Field,
            FieldDescription,
            FieldLabel,
            OTPField,
          } from "moduix";
          import { useId, useState } from "react";

          export function AlphanumericOTPField() {
            const id = useId();
            const [value, setValue] = useState("");

            return (
              <Field>
                <FieldLabel htmlFor={id}>Recovery code</FieldLabel>
                <FieldDescription>
                  Letters and numbers are allowed, for example <code>A7C9XZ</code>.
                </FieldDescription>
                <OTPField
                  id={id}
                  length={6}
                  value={value}
                  validationType="alphanumeric"
                  onValueChange={setValue}
                />
              </Field>
            );
          }
        `}
  </Preview.Code>
</Preview>

### Grouped Layout [#grouped-layout]

Use `groupSize` when a code should be presented in smaller visual groups.

<Preview>
  <OTPFieldGroupedLayoutExample />

  <Preview.Code>
    {`
          import {
            Field,
            FieldLabel,
            OTPField,
          } from "moduix";
          import { useId } from "react";

          export function GroupedOTPField() {
            const id = useId();

            return (
              <Field>
                <FieldLabel htmlFor={id}>Auth code</FieldLabel>
                <OTPField id={id} length={6} groupSize={3} />
              </Field>
            );
          }
        `}
  </Preview.Code>
</Preview>

### Placeholder Hints [#placeholder-hints]

Pass native `placeholder` props to the input slots and style them with `className`.

<Preview>
  <OTPFieldPlaceholderHintsExample />

  <Preview.Code>
    {`
          import { Field, FieldDescription, FieldLabel, OTPField } from "moduix";
          import { useId } from "react";
          import styles from "./otp-field.module.css";

          export function PlaceholderOTPField() {
            const id = useId();

            return (
              <Field>
                <FieldLabel htmlFor={id}>Verification code</FieldLabel>
                <FieldDescription>
                  Placeholder hints stay visible until the active slot is focused.
                </FieldDescription>
                <OTPField
                  id={id}
                  length={6}
                  inputProps={{
                    className: styles.placeholderInput,
                    placeholder: "•",
                  }}
                />
              </Field>
            );
          }
        `}
  </Preview.Code>

  <Preview.CSS>
    {`
          .placeholderInput {
            &::placeholder {
              color: var(--color-muted-foreground);
            }

            &:focus::placeholder {
              color: transparent;
            }
          }
        `}
  </Preview.CSS>
</Preview>

### Masked [#masked]

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

<Preview>
  <OTPFieldMaskedExample />

  <Preview.Code>
    {`
          import { Field, FieldLabel, OTPField } from "moduix";
          import { useId } from "react";

          export function MaskedOTPField() {
            const id = useId();

            return (
              <Field>
                <FieldLabel htmlFor={id}>PIN</FieldLabel>
                <OTPField id={id} length={4} mask />
              </Field>
            );
          }
        `}
  </Preview.Code>
</Preview>

### Field Validation [#field-validation]

Wrap the control in `Field` to show validation errors from props such as `required`.

<Preview>
  <OTPFieldValidationExample />

  <Preview.Code>
    {`
          import {
            Field,
            FieldError,
            FieldLabel,
            OTPField,
          } from "moduix";
          import { useId } from "react";

          export function ValidatedOTPField() {
            const id = useId();

            return (
              <Field name="verificationCode" validationMode="onBlur">
                <FieldLabel htmlFor={id}>Verification code</FieldLabel>
                <OTPField id={id} length={6} required />
                <FieldError match="valueMissing">Please enter the verification code.</FieldError>
              </Field>
            );
          }
        `}
  </Preview.Code>
</Preview>

### Auto Submit [#auto-submit]

Use `autoSubmit` to submit the owning form as soon as all slots are filled.

<Preview>
  <OTPFieldAutoSubmitExample />

  <Preview.Code>
    {`
          import { Field, FieldDescription, FieldLabel, OTPField } from "moduix";
          import { useId, useState } from "react";

          export function AutoSubmitOTPField() {
            const id = useId();
            const [completedValue, setCompletedValue] = useState("");
            const [submittedValue, setSubmittedValue] = useState("");

            return (
              <form
                onSubmit={(event) => {
                  event.preventDefault();
                  const formData = new FormData(event.currentTarget);
                  setSubmittedValue(String(formData.get("verificationCode") ?? ""));
                }}
              >
                <Field>
                  <FieldLabel htmlFor={id}>Verification code</FieldLabel>
                  <FieldDescription>
                    Last completed: {completedValue || "empty"}, submitted: {submittedValue || "empty"}
                  </FieldDescription>
                  <OTPField
                    id={id}
                    name="verificationCode"
                    length={6}
                    autoSubmit
                    onValueComplete={setCompletedValue}
                  />
                </Field>
              </form>
            );
          }
        `}
  </Preview.Code>
</Preview>

### Custom Sanitization [#custom-sanitization]

Set `validationType="none"` with `sanitizeValue` when pasted or typed values need custom normalization.

<Preview>
  <OTPFieldCustomSanitizationExample />

  <Preview.Code>
    {`
          import { Field, FieldDescription, FieldLabel, OTPField } from "moduix";
          import { useId, useState } from "react";

          export function CustomSanitizationOTPField() {
            const id = useId();
            const [value, setValue] = useState("");
            const [invalidValue, setInvalidValue] = useState("");

            return (
              <Field>
                <FieldLabel htmlFor={id}>Invite code</FieldLabel>
                <FieldDescription>
                  Last invalid attempt: {invalidValue || "none"}
                </FieldDescription>
                <OTPField
                  id={id}
                  length={6}
                  value={value}
                  validationType="none"
                  sanitizeValue={(nextValue) => nextValue.toUpperCase().replace(/[^A-Z0-9]/g, "")}
                  onValueChange={setValue}
                  onValueInvalid={setInvalidValue}
                />
              </Field>
            );
          }
        `}
  </Preview.Code>
</Preview>

### Custom Separator [#custom-separator]

Style generated slots with `inputProps` and pass any icon to `separator`.

<Preview>
  <OTPFieldCustomSeparatorExample />

  <Preview.Code>
    {`
          import {
            Field,
            FieldLabel,
            OTPField,
            SeparatorMarkIcon,
          } from "moduix";
          import { useId } from "react";
          import styles from "./otp-field.module.css";

          export function CustomSeparatorOTPField() {
            const id = useId();

            return (
              <Field>
                <FieldLabel htmlFor={id}>Styled code</FieldLabel>
                <OTPField
                  id={id}
                  length={6}
                  groupSize={3}
                  inputProps={{ className: styles.customInput }}
                  separator={<SeparatorMarkIcon />}
                  className={styles.customRoot}
                />
              </Field>
            );
          }
        `}
  </Preview.Code>

  <Preview.CSS>
    {`
          .customInput {
            --otp-field-input-width: 3rem;
            --otp-field-input-height: 3rem;
            --otp-field-font-size: var(--text-xl);
            --otp-field-bg-filled: var(--color-muted);
          }

          .customRoot {
            --otp-field-separator-size: 1.5rem;
            --otp-field-separator-color: var(--color-primary);
          }
        `}
  </Preview.CSS>
</Preview>
