Photon Design System

OTP Field

A one-time-code input — a row of single-character cells with paste, arrow-key navigation, and auto-advance. Built on Base UI OTPField, it defaults to numeric input and autocomplete="one-time-code" so a texted code can be filled in one tap.

Import

import { OtpField } from "@photon-ai/pho-ui/components/otp-field";

Basic

Set length for the number of cells. Give the field an aria-label (or an external <label>) — a bare row of boxes is otherwise unlabeled.

<OtpField length={6} aria-label="Verification code" />

Masked

mask hides the entered characters, like a password field — reach for it when the code is sensitive and shouldn’t sit visible on screen.

<OtpField length={6} mask aria-label="Verification code" />

Controlled

Pass value with onValueChange to control the code. onValueChange fires on every edit — typing, delete, and paste — so you can read the code as it fills. For a “code is complete” moment, use onValueComplete, or set autoSubmit to submit the owning form the instant every cell is filled.

Type a code

const [code, setCode] = useState("");

<OtpField
  length={6}
  value={code}
  onValueChange={setCode}
  aria-label="Verification code"
/>;

Props

  • length — number (required) — number of single-character cells
  • value / defaultValue — string — the controlled / uncontrolled code
  • onValueChange — (value, eventDetails) => void — fires on every value change; the reason (input-change, input-clear, input-paste, keyboard) is on eventDetails
  • onValueComplete — (value, eventDetails) => void — fires when every cell is filled
  • onValueInvalid — (value, eventDetails) => void — fires when entered text is rejected by validationType or normalizeValue
  • validationType — "numeric" · "alpha" · "alphanumeric" · "none" (default "numeric") — which characters the cells accept
  • mask — boolean (default false) — hide entered characters
  • autoSubmit — boolean (default false) — submit the owning form when the code completes
  • required — boolean (default false) — require a value before form submit
  • disabled — boolean (default false) — ignore user interaction
  • readOnly — boolean (default false) — prevent changes but keep the value focusable
  • name — string — identifies the field on form submit
  • autoComplete — string (default "one-time-code") — applied to the first cell and the hidden input
  • inputMode — the virtual-keyboard hint for the cells
  • className — string — class applied to the cell row

Accessibility

  • Labelling — the field has no built-in label; pass aria-label or associate an external <label>. Each cell is a native text input, and the row shares the one-time-code autocomplete so OS code-autofill works.
  • State — disabled and readOnly are reflected on the cells (dimmed while disabled), and the root exposes data-complete / data-filled / data-focused for styling. Wrapped in a Field, cells pick up data-valid / data-invalid.

Keyboard:

  • Type — enter a character and focus auto-advances to the next cell.
  • Backspace / Delete — clear the current cell (Backspace steps back once empty).
  • Left / Right arrows — move between cells (mirrored under RTL).
  • Home / End — jump to the first / last cell.
  • Paste — a full code pasted into any cell fills the cells across the row.

Focus shows a 2px brand ring on the active cell (:focus-visible).

Best practices

  • Match length to the real code length so paste fills exactly and completion fires at the right moment.
  • Provide an aria-label (or an external <label>); a bare row of boxes is unlabeled.
  • Keep validationType matched to the code — leave it numeric for digit codes so the cells reject stray letters and phones show a number pad.
  • Reach for autoSubmit (or onValueComplete) instead of a separate submit button when the code length is fixed and known.
  • Don’t split the code into a text input plus formatting — the OTP field already handles paste of the full code across cells.