OTP Field
Primitive

OTP Field

One-time password input coordination across multiple visible cells and one hidden form value.

Live behavior

OTP Field in motion

Interactive
Atom behavior · App-owned appearance
Preparing behavior…

Interact with the specimen and inspect the behavior Atom contributes.

waiting for input

When to Use

Use OTPField for a short verification code that is entered one character at a time and may be pasted from a message. Use PasswordToggleField for a reusable secret chosen by the person, and Input when the value belongs in one normal text box instead of visually separated cells.

Features

The behavior Atom owns before your product adds appearance.

  • Controlled and uncontrolled full value.
  • Coordinates rendered input cells and assigns indexes from their render order.
  • Optional explicit input indexes.
  • Roving tab stop so the field behaves as one logical control.
  • Paste distribution across cells.
  • Arrow, Backspace, Delete, Home, and End navigation.
  • Numeric, alphabetic, alphanumeric, or custom pattern filtering.
  • Hidden input for native form submission.
  • Optional masking, completion callback, auto-focus, and form submission.

Import

tsx
import { OTPField } from "@flowstack-ui/atom";

Anatomy

tsx
<OTPField.Root>
  <OTPField.Input />
  <OTPField.Separator />
  <OTPField.Input />
</OTPField.Root>

API Reference

Root

Owns the complete code value, filtering, cell registration, focus movement, Field state, completion behavior, and optional hidden form input.

PropTypeDefault
childrenReactNoderequired
valuestring-
defaultValuestring""
onValueChange(value: string) => void-
onComplete(value: string) => void-
lengthnumber6
type"numeric" | "alphabetic" | "alphanumeric""numeric"
patternRegExpDerived from type
maskboolean | stringfalse
namestring-
formstring-
inputIdstringGenerated or inherited from Field
getInputLabel(index, length, type) => stringGenerated English position label
autoFocusbooleanfalse
autoSubmitbooleanfalse
disabledbooleanfalse
readOnlybooleanfalse
requiredbooleanfalse
invalidbooleanfalse
validationBehavior"inline" | "native"Field/Form value or "native"
asChildbooleanfalse
renderRenderProp-
ARIA attributeValues
aria-labelNative value, or "Verification code" without an external label
aria-labelledbyInherited Field label ID when no direct label is provided
aria-describedbyNative value or Field descriptions
aria-invalidPresent when invalid
Data attributeValues
[data-slot]"otp-field"
[data-disabled]Present when disabled
[data-readonly]Present when read-only
[data-required]Present when required
[data-invalid]Present when invalid

Input

Renders one visible character cell, joins the roving tab stop, and delegates typing, paste, deletion, and focus movement to Root.

PropTypeDefault
childrenReactNode-
indexnumberDOM order
aria-labelstringGenerated from index and length
asChildbooleanfalse
renderRenderProp-
ARIA attributeValues
aria-label"Digit N of length" or "Character N of length" by default
aria-invalidPresent when the root is invalid
aria-requiredPresent when the root is required
Data attributeValues
[data-slot]"otp-field-input"
[data-index]Zero-based cell index
[data-filled]Present when the cell has a value
[data-disabled]Present when disabled
[data-readonly]Present when read-only
[data-invalid]Present when invalid

Separator

Decorative separator between cells.

PropTypeDefault
childrenReactNode-
indexnumber-
asChildbooleanfalse
renderRenderProp-
ARIA attributeValues
aria-hiddentrue
Data attributeValues
[data-slot]"otp-field-separator"
[data-index]Value from index when provided

The package exports useOTPFieldContext and its provider for advanced compound parts. It also exports getOTPFieldPattern, isOTPFieldCharAccepted, getOTPFieldChars, filterOTPFieldValue, and getOTPFieldDisplayChar for using the same filtering and display rules outside the rendered field.

Examples

Six Digit Code

tsx
import { OTPField } from "@flowstack-ui/atom";

export function VerificationCode() {
  return (
    <OTPField.Root name="code" length={6}>
      {Array.from({ length: 6 }, (_, index) => (
        <OTPField.Input key={index} />
      ))}
    </OTPField.Root>
  );
}

Grouped Code

tsx
import { OTPField } from "@flowstack-ui/atom";

export function GroupedCode() {
  return (
    <OTPField.Root length={6} onComplete={(code) => console.log(code)}>
      <OTPField.Input />
      <OTPField.Input />
      <OTPField.Input />
      <OTPField.Separator>-</OTPField.Separator>
      <OTPField.Input />
      <OTPField.Input />
      <OTPField.Input />
    </OTPField.Root>
  );
}

Accessibility

The first visible cell owns required validity and aria-required for the logical OTP value; the combined named input remains submission-only. A validation attempt is mirrored across Root and every cell. Inline behavior suppresses the browser bubble.

The root uses role="group" and the visible inputs use roving tabIndex, so Tab enters the OTP field once. Each input receives a generated position label, and the separator is hidden from assistive technology. The group does not carry aria-required, because that attribute is unsupported on role="group". Give the group a clear label through native aria-label/aria-labelledby or Field. The first visible cell owns required validity and anchors native browser feedback. The combined hidden native input is submission-only; uncontrolled content resets to defaultValue.

Use getInputLabel to localize every generated cell position label. A direct aria-label on an Input still overrides the generated label for that cell.

KeyDescription
TabEnters or leaves the OTP field as one logical control.
ArrowRightMoves to the next cell.
ArrowLeftMoves to the previous cell.
HomeMoves to the first cell.
EndMoves to the last cell.
BackspaceClears the current cell or moves backward when empty.
DeleteClears the current cell.
PasteDistributes accepted characters across cells.

Changelog

Unreleased

0.24.0

  • Added public Agent Knowledge for component selection, required composition, recurring mistakes, and validation.

0.19.2

  • Removed unsupported aria-required from the role="group" root while preserving required semantics and native validity on the visible cells.

0.19.0

  • Added getInputLabel for localizing generated cell position labels.

0.6.16

  • Explicitly scrolled inline validation-directed focus into view.

0.6.15

  • Exposed inline validation-directed focus through [data-focus-visible] until blur.

0.6.13

  • Added logical-field inline/native validation presentation and synchronized invalid state across Root, visible cells, Field, and Form.

0.6.12

  • Moved required validity to the first visible cell and made the combined named value submission-only.

0.5.0

  • Removed ariaLabel/ariaDescribedBy in favor of native ARIA and added a required-capable native combined-value input with uncontrolled reset.

0.1.0

  • Initial Atom release.