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.
Sent by SMS to (415) •••-0132.
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.
Examples
Every example is a real file you can copy. They all wear the skin, mode and brand from the bar above.
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.
Expired code
When the server rejects the code, set invalid on the Field and say what to do next, not just that it failed.
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.
Confirm your phone
We texted a code to (415) 555-0132.
Confirm transfer
$1,250.00 to Marina Alves, Northwind Bank.
Open the Northwind app and tap Security code.
Guidelines
Sent by SMS to (415) •••-0132.
People know which app to open, and notice a wrong number.
From the email, the SMS or the authenticator? People switch apps to guess.
They compare them with the SMS at a glance.
It expires in minutes and works once; hiding it only makes typos harder to spot.
Props
separator cells (3 → 123–456).Field + FieldLabel.type directly to individual <OTPField.Input> parts to use a custom
input type.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.details.reason says which.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
Tokens it reads
Color
Shape & size
Type & motion
This component only
/* 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
- The first cell is named by the Field label; the others by getCellLabel, which defaults to English ("Character 2 of 6").
- autocomplete="one-time-code" is on by default, so iOS and Android offer the code from the SMS above the keyboard. Keep it for SMS and email codes.
- The separator is aria-hidden: screen readers hear the cells, not the dashes.
- An invalid Field marks every cell with aria-invalid, and each cell is described by the FieldDescription and FieldError. Keep the error right below the cells, where people look after typing.