// preview in
saved for every page

OTP field

A one-time code split into one cell per character. Typing moves forward, Backspace goes back and a paste or SMS autofill fills every cell at once.

Built on Base UIPattern HTML input (autocomplete=one-time-code)Client componentSource ↗
minimal · light · #3fa34d

Sent by SMS to (415) •••-0132.

mask
disabled

Installation

Set up Camaleo once (Installation), then import what you need. Put it in a Field to give it a label.

import { OtpField, Field, FieldLabel } from "@camaleo/react";

Anatomy

A row of cells, one input each. separator adds a dash between groups, drawn for the eye only.

482–913
[data-slot="otp-field"][data-slot="otp-separator"][data-slot="otp-input"]

Examples

Every example is a real file you can copy. They all wear the skin, mode and brand from the bar above.

minimal · light · #3fa34d

Sent by SMS to (415) •••-0132.

Paste 482913, or type any other code.

Paste, verify and error

Paste a code. onValueComplete runs once the last cell fills, so you can check it right away; a wrong code marks every cell invalid.

This code expired after 10 minutes. Request a new one.

Expired code

When the server rejects the code, set invalid on the Field and say what to do next, not just that it failed.

Nothing sent yet. Fill the last cell.

Submit when complete

Type six digits. autoSubmit submits the form on the last one, and name sends the whole code through one hidden input.

The 4 digits you use at ATMs.

Masked PIN

mask hides each digit after it is typed. Use it for a PIN someone could read over a shoulder, not for a code that expires in minutes.

Letters and numbers, from the codes you saved when you turned on two-step verification.

Letters and numbers

validationType="alphanumeric" accepts letters and opens the full keyboard; normalizeValue uppercases them so "k7p" matches.

Translated cell names

The first cell takes the Field label and the others get getCellLabel, so pass one in the page's language.

Sending a new code to (415) •••-0132…

Disabled while sending

Disable it while a new code is on its way, and say so in the description, so nobody types the old one.

In context

These are whole pieces of UI you can copy, in the skin you picked.

Two-step verification

Enter the 6-digit code from your authenticator app.

Use a recovery code instead
Two-step verificationCard · Field · OtpField · Link · Button

Confirm your phone

We texted a code to (415) 555-0132.

Confirm a phone numberCard · Field · OtpField · Button

Confirm transfer

$1,250.00 to Marina Alves, Northwind Bank.

Open the Northwind app and tap Security code.

Confirm a transferCard · Field · OtpField · Button

Guidelines

Sent by SMS to (415) •••-0132.

DoSay where the code went.

People know which app to open, and notice a wrong number.

Don’tJust "Code".

From the email, the SMS or the authenticator? People switch apps to guess.

DoShow the digits as people type.

They compare them with the SMS at a glance.

Don’tMask a one-time code.

It expires in minutes and works once; hiding it only makes typos harder to spot.

Props

PropTypeDefaultDescription
separatornumber—Draws a separator after every separator cells (3 → 123–456).
getCellLabel(index: number, length: number) => string(index, length) => `Character ${index + 1} of ${length}`Accessible name of cells 2 onwards (translatable). The first cell takes the field's label, so label the whole field with Field + FieldLabel.
autoCompletestring'one-time-code'The input autocomplete attribute. Applied to the first slot and hidden validation input.
length*number—The number of OTP input slots. Required so the root can clamp values, detect completion, and generate consistent validation markup before all slots hydrate.
autoSubmitbooleanfalseWhether to submit the owning form when the OTP becomes complete.
maskbooleanfalseWhether the slot inputs should mask entered characters. Pass type directly to individual <OTPField.Input> parts to use a custom input type.
validationType"numeric" | "alpha" | "alphanumeric" | "none"'numeric'The type of input validation to apply to the OTP value.
normalizeValue(value: string) => string—Function that normalizes the OTP value after whitespace and validationType filtering. It runs whenever OTP Field normalizes a value, including initial/default values, controlled values, and user edits. The returned value is filtered by validationType again, then clamped to length. It should be idempotent because OTP Field may normalize the same value more than once while handling edits, storing state, and rendering controlled or uncontrolled values. Non-idempotent normalizers can compound across those normalization passes. Characters rejected while normalizing typed or pasted text are reported through onValueInvalid.
requiredbooleanfalseWhether the user must enter a value before submitting a form.
disabledbooleanfalseWhether the component should ignore user interaction.
namestring—Identifies the field when a form is submitted.
valuestring—The OTP value.
defaultValuestring—The uncontrolled OTP value when the component is initially rendered.
onValueChange(value: string, details) => void—Runs on every change, from typing, paste or the keyboard. details.reason says which.
onValueComplete(value: string, details) => void—Runs when every cell is filled, after onValueChange. Check the code here.

Base UI's other root props pass through, such as readOnly, inputMode, form and onValueInvalid.

Styling hooks

Restyle it without forking: target the attributes, or set the tokens on a parent, a skin or the field itself.

Attributes

data-slot"otp-field" · "otp-input" · "otp-separator"
data-filledon a cell with a character; on the root when any cell has one
data-completepresent when every cell is filled
data-invalidpresent when its Field is invalid
data-disabledpresent when disabled
data-focusedpresent while any cell has focus

Tokens it reads

Color

--cml-border-control--cml-danger--cml-fg--cml-fg-muted--cml-field-bg--cml-field-border-sides--cml-field-shadow--cml-focus-ring

Shape & size

--cml-field-border-width--cml-field-height--cml-field-padding--cml-font-size-field--cml-radius-control--cml-radius-field

Type & motion

--cml-duration-fast--cml-ease-standard--cml-font-field

This component only

--cml-corner-shape--cml-disabled-opacity--cml-otp-cell-height--cml-otp-cell-width--cml-otp-font--cml-otp-font-size--cml-otp-font-weight--cml-otp-gap--cml-otp-separator
/* smaller cells, only in the payment sheet */
.payment-sheet [data-slot="otp-field"] {
  --cml-otp-cell-width: 2.25rem;
  --cml-otp-cell-height: 2.5rem;
  --cml-otp-gap: 0.375rem;
}

Accessibility

0–9 · lettersFills the cell and moves to the next one. Characters that validationType rejects are ignored.
BackspaceClears the cell and moves back to the previous one.
Arrow Left · Arrow RightMoves between cells without changing them.
Ctrl + V · Cmd + VPastes a whole code and fills every cell from the first.
TabMoves focus into the field and on to the next control.
Was this page helpful? · Edit this page on GitHub