OTP field
A one-time code entered one character per slot. Supports paste and one-time-code autofill. For longer values, such as API keys, use Input.
Code
import {Field, OTPField} from '@earendil-works/elanor-react';
const LENGTH = 6;
export default function OTPFieldDemo() {
return (
<Field.Root name="code">
<Field.Label>Verification code</Field.Label>
<OTPField.Root length={LENGTH}>
{Array.from({length: LENGTH}, (_, index) => (
<OTPField.Input
key={index}
aria-label={index === 0 ? undefined : `Character ${index + 1} of ${LENGTH}`}
/>
))}
</OTPField.Root>
</Field.Root>
);
} Usage
import {Field, OTPField} from '@earendil-works/elanor-react';
<Field.Root name="code">
<Field.Label>Verification code</Field.Label>
<OTPField.Root length={6}>
<OTPField.Input />
<OTPField.Input aria-label="Character 2 of 6" />
{/* … one Input per character */}
</OTPField.Root>
</Field.Root>;
Field.Label labels the first slot; give the others an aria-label. The field submits one value under the Field.Root’s name.
Validation
validationType defaults to numeric and sets a native pattern, so an incomplete code fails with patternMismatch. Give it a Field.Error, otherwise the browser’s message shows.
Code
import {Button, Field, Form, OTPField, Stack} from '@earendil-works/elanor-react';
const LENGTH = 6;
export default function OTPFieldValidation() {
return (
<Form>
<Stack gap="medium">
<Field.Root name="code">
<Field.Label>Verification code</Field.Label>
<OTPField.Root length={LENGTH} required>
{Array.from({length: LENGTH}, (_, index) => (
<OTPField.Input
key={index}
aria-label={index === 0 ? undefined : `Character ${index + 1} of ${LENGTH}`}
/>
))}
</OTPField.Root>
<Field.Error match="valueMissing">Enter the code from the email.</Field.Error>
<Field.Error match="patternMismatch">Enter all 6 digits.</Field.Error>
</Field.Root>
<Button type="submit">Verify</Button>
</Stack>
</Form>
);
} Grouped
Code
import {Field, OTPField} from '@earendil-works/elanor-react';
const LENGTH = 6;
function slot(index: number) {
return (
<OTPField.Input
key={index}
aria-label={index === 0 ? undefined : `Character ${index + 1} of ${LENGTH}`}
/>
);
}
export default function OTPFieldGrouped() {
return (
<Field.Root name="code">
<Field.Label>Verification code</Field.Label>
<OTPField.Root length={LENGTH}>
{[0, 1, 2].map(slot)}
<OTPField.Separator />
{[3, 4, 5].map(slot)}
</OTPField.Root>
</Field.Root>
);
} Alphanumeric
Use validationType="alphanumeric" for recovery and invite codes. normalizeValue runs on every edit, so it must return the same value when run twice; define it outside the component.
Code
import {Field, OTPField} from '@earendil-works/elanor-react';
const LENGTH = 8;
function uppercase(value: string) {
return value.toUpperCase();
}
export default function OTPFieldAlphanumeric() {
return (
<Field.Root name="recoveryCode">
<Field.Label>Recovery code</Field.Label>
<OTPField.Root length={LENGTH} validationType="alphanumeric" normalizeValue={uppercase}>
{Array.from({length: LENGTH}, (_, index) => (
<OTPField.Input
key={index}
aria-label={index === 0 ? undefined : `Character ${index + 1} of ${LENGTH}`}
/>
))}
</OTPField.Root>
</Field.Root>
);
} Full width
Slots are square. Override their size with a class to fill the container.
Code
import {Field, OTPField} from '@earendil-works/elanor-react';
import styles from './otp-field.module.css';
const LENGTH = 6;
export default function OTPFieldFullWidth() {
return (
<Field.Root name="code">
<Field.Label>Verification code</Field.Label>
<OTPField.Root length={LENGTH}>
{Array.from({length: LENGTH}, (_, index) => (
<OTPField.Input
key={index}
className={styles.stretch}
aria-label={index === 0 ? undefined : `Character ${index + 1} of ${LENGTH}`}
/>
))}
</OTPField.Root>
</Field.Root>
);
} .stretch {
flex: 1 1 0;
inline-size: auto;
} API
OTPField.Root
Groups the slots and owns the value. Render one Input per character of length.
Accepts Base UI OTPField.Root props.
OTPField.Input
One character slot. Label every slot after the first, for example "Character 2 of 6".
Accepts Base UI OTPField.Input props.
OTPField.Separator
Splits the slots into groups, such as 123–456.
Accepts Base UI Separator props.