OTP Input
Capture a short, fixed-length one-time passcode (OTP), for two-factor codes,
email verification codes, magic-link codes, and similar flows. Built on top of
input-otp, it renders a single
hidden input that handles paste, autofill, and IME correctly while displaying
each character in its own styled slot.
Pair with Field.* for label/description/error wiring in forms:
Enter the 6-digit code we emailed you.
Or render the control on its own:
With TanStack Form
Use @tanstack/react-form with zod to validate the hidden OTP input while Field.Control handles label, name, and validation ARIA.
Single group
Render all slots in a single group with no separator.
Digits only
Restrict input to numeric characters using the pattern prop and the
REGEXP_ONLY_DIGITS helper.
Disabled
Pass disabled to the root to disable the entire input.
Validation
Pass validation="error", "success", or "warning" on OtpInput.Root to
recolor each group's outer borders and the active focus ring with the matching
validation hue. validation="error" also sets aria-invalid on the
underlying input so assistive tech announces the failure state.
validation may be a literal value or a function that returns a value. Since
the prop also accepts false, form-library state can short-circuit validation
styling, e.g.
validation={field.state.meta.errors.length > 0 && "error"}.
Composition
Compose the parts of an OtpInput together to build your own:
Polymorphism
OtpInput.Group and OtpInput.Separator accept an asChild prop. When true,
the part renders its single child instead of its default div, forwarding all
class names, data-* attributes, and the ref onto that child. Use this when you
need a different underlying element: for example, rendering the separator as a
semantic span, or wrapping a group in a styled label.
Accessibility
The hidden <input> carries the value and the focus, so it needs an accessible name. Inside a Field.Item, Field.Label supplies it through the Field.Item id. On its own, pass aria-label to OtpInput.Root. Every example above that renders the control without a Field.Label does so.
Translation
Every OtpInput.Slot carries translate="no", so a browser translation engine skips the character inside it. A translated passcode character is wrong, and the reader types it back anyway.
The attribute also protects the render. Google Translate wraps each text node in a <font>, which detaches the node React holds. The next keystroke then makes React remove a node its parent no longer owns, and the DOM raises NotFoundError. Without the guard, a backspace on a translated page tears down the root.
OtpInput.Slot omits the translate prop from its type, so no call site can turn the guard off. A passcode character is never translatable, unlike Code, which also styles terms that are not code and therefore takes the override.
Browser translation covers what an engine does to a React DOM, and the rule your own components need.
API Reference
OtpInput.Root
The root of the OTP input. Wraps the hidden input that captures keystrokes,
paste, and autofill, and exposes per-slot state to descendant OtpInput.Slot
parts via context.
All props from the underlying OTPInput component, including standard input props, plus:
OtpInput.Group
Groups one or more OtpInput.Slot parts into a visually-connected segment with shared rounded corners and joined borders.
All props from div, plus:
OtpInput.Slot
A single character slot. Must be rendered inside an OtpInput.Root. Reads its character, active state, and fake caret position from the root via context. Always renders translate="no" — see Translation.
All props from div except translate, plus:
OtpInput.Separator
A visual separator between two OtpInput.Group segments. Renders a minus icon by default; pass children to override.
All props from div, plus:
Pattern helpers
Re-exported from input-otp for convenience: