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.
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.
| Property | Default | Description |
|---|---|---|
| --otp-field-bg | var(--color-background) | Controls input background. |
| --otp-field-bg-filled | var(--otp-field-bg) | Controls filled input background. |
| --otp-field-border-color | var(--color-border) | Controls default input border color. |
| --otp-field-border-color-complete | var(--otp-field-border-color) | Controls border color when the field is complete. |
| --otp-field-border-color-invalid | var(--color-destructive) | Controls invalid border and focus ring color. |
| --otp-field-border-width | var(--border-width-sm) | Controls input border width. |
| --otp-field-color | var(--color-foreground) | Controls input text color. |
| --otp-field-disabled-opacity | var(--opacity-disabled) | Controls disabled opacity. |
| --otp-field-focus-ring-color | var(--color-ring) | Controls focus ring color. |
| --otp-field-focus-ring-offset | -1px | Controls focus ring offset. |
| --otp-field-focus-ring-width | var(--otp-field-border-width) | Controls focus ring width. |
| --otp-field-input-height | var(--otp-field-input-size) | Controls input slot height. |
| --otp-field-input-padding-x | 0 | Controls horizontal input padding. |
| --otp-field-input-padding-y | 0 | Controls vertical input padding. |
| --otp-field-input-size | 2.5rem | Controls square input slot size. |
| --otp-field-input-width | var(--otp-field-input-size) | Controls input slot width. |
| --otp-field-font-size | var(--text-lg) | Controls input font size. |
| --otp-field-font-weight | var(--weight-medium) | Controls input font weight. |
| --otp-field-gap | var(--spacing-2) | Controls spacing between slots and separators. |
| --otp-field-line-height | var(--line-height-text-lg) | Controls input text line height. |
| --otp-field-max-width | none | Controls the root OTP field max width. |
| --otp-field-placeholder-color | var(--color-muted-foreground) | Controls placeholder color. |
| --otp-field-radius | var(--radius-md) | Controls input corner radius. |
| --otp-field-separator-color | var(--color-muted-foreground) | Controls separator color. |
| --otp-field-separator-height | var(--otp-field-separator-size) | Controls separator height. |
| --otp-field-separator-size | 1rem | Controls separator wrapper width and height. |
| --otp-field-separator-width | var(--otp-field-separator-size) | Controls separator width. |
| --otp-field-transition | var(--transition-default) | Controls input state transitions. |
| --otp-field-width | auto | Controls the root OTP field width. |
Interactive variables scoped for docs preview without changing size scale tokens.
| Property | Value | Default | Description |
|---|---|---|---|
| --otp-field-bg | var(--color-background) | Controls input background. | |
| --otp-field-bg-filled | var(--otp-field-bg) | Controls filled input background. | |
| --otp-field-border-color | var(--color-border) | Controls default border color. | |
| --otp-field-border-width | var(--border-width-sm) | Controls input border width. | |
| --otp-field-color | var(--color-foreground) | Controls input text color. | |
| --otp-field-focus-ring-color | var(--color-ring) | Controls focus ring color. | |
| --otp-field-gap | var(--spacing-2) | Controls spacing between slots and separators. | |
| --otp-field-radius | var(--radius-md) | Controls input corner radius. | |
| --otp-field-separator-color | var(--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} />| 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
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.
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.
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.
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);}