OTP input
PortedA one-time code, one box per character. Typing moves to the next box and Backspace goes back; a paste or the browser's autofill (autocomplete="one-time-code") fills every box. Digits only, unless validationType says otherwise. onValueComplete runs when the last box is filled. Native <div> attributes and ref pass through.
import { OTPInput } from "@etched/monet/otp-input";
export function Preview() {
return <OTPInput aria-label="Verification code" groupSize={3} />;
}Installation
import { OTPInput } from "@etched/monet/otp-input";Examples
Verification code
Typing moves to the next box, Backspace goes back, and a paste fills every box. onValueComplete runs when the last box is filled.
We sent a 6-digit code to ana@etched.ai.
import { useState } from "react";
import { OTPInput } from "@etched/monet/otp-input";
export function VerificationCode() {
const [code, setCode] = useState("");
const [checked, setChecked] = useState("");
return (
<OTPInput
label="Verification code"
groupSize={3}
value={code}
onValueChange={(value) => {
setCode(value);
setChecked("");
}}
onValueComplete={setChecked}
description={checked ? `Checking ${checked}…` : "We sent a 6-digit code to ana@etched.ai."}
/>
);
}Length, groups and characters
length sets the boxes and groupSize splits them. validationType="alphanumeric" takes letters too; normalizeValue can make them upper case.
Shown as dots.
import { OTPInput } from "@etched/monet/otp-input";
export function LengthGroupsAndCharacters() {
return (
<div className="grid">
<OTPInput label="Backup code" length={8} groupSize={4} validationType="alphanumeric" normalizeValue={(value) => value.toUpperCase()} />
<OTPInput label="Bench PIN" length={4} mask description="Shown as dots." />
</div>
);
}Sizes
import { OTPInput } from "@etched/monet/otp-input";
export function Sizes() {
return (
<div className="grid">
<OTPInput aria-label="Code (small)" size="sm" groupSize={3} />
<OTPInput aria-label="Code" groupSize={3} />
</div>
);
}States
That code has expired. Ask for a new one.
import { OTPInput } from "@etched/monet/otp-input";
export function States() {
return (
<div className="grid">
<OTPInput label="Sign-in code" groupSize={3} defaultValue="482913" error="That code has expired. Ask for a new one." />
<OTPInput label="Code sent to the bench" groupSize={3} defaultValue="105377" readOnly />
<OTPInput label="Code (locked)" groupSize={3} disabled />
</div>
);
}Accessibility
- Typing fills a box and moves on;
Backspacegoes back.ArrowLeft/ArrowRightmove one box;Home/Endgo to the start and end of the code. - Only one box is in the tab order, so
Tableaves the code in one step. A paste or one-time-code autofill fills every box. - A
groupnamed bylabel, or byaria-labelwhen no label shows. The first box takes that name; the others are named "Character 2 of 6" and so on, or bycharacterLabel. descriptionanderrordescribe the group, and an error setsaria-invalidon each box.- Give it a
label, or anaria-labelwhen no label shows.
API reference
| Prop | Type | Default | Description |
|---|---|---|---|
| label | ReactNode | – | Visible label. Without one, give the control an aria-label. |
| labelTooltip | ReactNode | – | More about the field, behind an info button after the label. |
| labelTooltipLabel | string | "More information" | Names the info button. |
| description | ReactNode | – | Help text below the control. Hidden while an error shows. |
| error | FieldError | – | |
| length | number | 6 | How many characters, one box each. |
| groupSize | number | all in one group | Boxes per group, with a separator between groups, such as 3 for "123–456". |
| size | "sm" | "base" | "base" | 28 or 36 px boxes, as Input. |
| characterLabel | (index: number, length: number) => string | `Character ${index + 1} of ${length}` | Names each box after the first, which takes the field's name. |
| className | string | – | Class on the outer element, for layout. |