moduix

OTP Field

A one-time password input composed of individual character slots.

API Reference

Original primitive API

Behavior, accessibility details, and low-level props are documented by Base UI.

Base UI API

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.

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>  );}

Full list of component variables available for project-level overrides.

PropertyDefaultDescription
--otp-field-bgvar(--color-background)Controls input background.
--otp-field-bg-filledvar(--otp-field-bg)Controls filled input background.
--otp-field-border-colorvar(--color-border)Controls default input border color.
--otp-field-border-color-completevar(--otp-field-border-color)Controls border color when the field is complete.
--otp-field-border-color-invalidvar(--color-destructive)Controls invalid border and focus ring color.
--otp-field-border-widthvar(--border-width-sm)Controls input border width.
--otp-field-colorvar(--color-foreground)Controls input text color.
--otp-field-disabled-opacityvar(--opacity-disabled)Controls disabled opacity.
--otp-field-focus-ring-colorvar(--color-ring)Controls focus ring color.
--otp-field-focus-ring-offset-1pxControls focus ring offset.
--otp-field-focus-ring-widthvar(--otp-field-border-width)Controls focus ring width.
--otp-field-input-heightvar(--otp-field-input-size)Controls input slot height.
--otp-field-input-padding-x0Controls horizontal input padding.
--otp-field-input-padding-y0Controls vertical input padding.
--otp-field-input-size2.5remControls square input slot size.
--otp-field-input-widthvar(--otp-field-input-size)Controls input slot width.
--otp-field-font-sizevar(--text-lg)Controls input font size.
--otp-field-font-weightvar(--weight-medium)Controls input font weight.
--otp-field-gapvar(--spacing-2)Controls spacing between slots and separators.
--otp-field-line-heightvar(--line-height-text-lg)Controls input text line height.
--otp-field-max-widthnoneControls the root OTP field max width.
--otp-field-placeholder-colorvar(--color-muted-foreground)Controls placeholder color.
--otp-field-radiusvar(--radius-md)Controls input corner radius.
--otp-field-separator-colorvar(--color-muted-foreground)Controls separator color.
--otp-field-separator-heightvar(--otp-field-separator-size)Controls separator height.
--otp-field-separator-size1remControls separator wrapper width and height.
--otp-field-separator-widthvar(--otp-field-separator-size)Controls separator width.
--otp-field-transitionvar(--transition-default)Controls input state transitions.
--otp-field-widthautoControls the root OTP field width.

Interactive variables scoped for docs preview without changing size scale tokens.

PropertyValueDefaultDescription
--otp-field-bgvar(--color-background)Controls input background.
--otp-field-bg-filledvar(--otp-field-bg)Controls filled input background.
--otp-field-border-colorvar(--color-border)Controls default border color.
--otp-field-border-widthvar(--border-width-sm)Controls input border width.
--otp-field-colorvar(--color-foreground)Controls input text color.
--otp-field-focus-ring-colorvar(--color-ring)Controls focus ring color.
--otp-field-gapvar(--spacing-2)Controls spacing between slots and separators.
--otp-field-radiusvar(--radius-md)Controls input corner radius.
--otp-field-separator-colorvar(--color-muted-foreground)Controls separator color.

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.

OTPField[length]
├─ OTPFieldInput
├─ OTPFieldInput
├─ OTPFieldSeparator (optional, from groupSize)
└─ OTPFieldInput
<OTPField id={id} length={6} groupSize={3} />
PartRole
OTPFieldRoot state machine. Controls value, validation, completion callbacks, masking, and automatic slots.
OTPFieldInputVisible character slot. Rendered automatically unless custom children are provided.
OTPFieldSeparatorOptional 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

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

Alphanumeric

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

Letters and numbers are allowed, for example A7C9XZ.

Current value: empty
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>  );}

Grouped Layout

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

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>  );}

Placeholder Hints

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

Placeholder hints stay visible until the active slot is focused.

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>  );}
.placeholderInput {  &::placeholder {    color: var(--color-muted-foreground);  }  &:focus::placeholder {    color: transparent;  }}

Masked

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

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>  );}

Field Validation

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

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>  );}

Auto Submit

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

Form submits automatically when all slots are filled.

Last completed value: emptyLast submitted value: empty
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>  );}

Custom Sanitization

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

validationType="none" with custom sanitization.

Current value: emptyLast invalid attempt: none
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>  );}

Custom Separator

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

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>  );}
.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);}

On this page