OTP Field

A one-time password input split into individual character slots with automatic focus management.

Anatomy

Import and assemble the component. length is required so the field can size, validate, and detect completion before all slots hydrate.

1import { OTPField } from "@raystack/apsara";
2
3<OTPField length={6}>
4 <OTPField.Input />
5 <OTPField.Input />
6 <OTPField.Input />
7 <OTPField.Separator />
8 <OTPField.Input />
9 <OTPField.Input />
10 <OTPField.Input />
11</OTPField>

API Reference

Root

Groups all parts of the field and manages their state.

Prop

Type

Input

An individual character slot. Render one per slot (typically using Array.from).

Prop

Type

Separator

A visual separator between slot groups, styled to fit between OTP slots.

Prop

Type

Examples

With separator

Group slots visually with OTPField.Separator to make long codes easier to read.

1<OTPField length={6}>
2 {Array.from({ length: 3 }, (_, i) => (
3 <OTPField.Input key={i} aria-label={`Character ${i + 1} of 6`} />
4 ))}
5 <OTPField.Separator />
6 {Array.from({ length: 3 }, (_, i) => (
7 <OTPField.Input key={i + 3} aria-label={`Character ${i + 4} of 6`} />
8 ))}
9</OTPField>

Masked

Use mask to obscure entered characters — useful for sensitive codes.

1<OTPField length={6} mask>
2 {Array.from({ length: 6 }, (_, i) => (
3 <OTPField.Input key={i} aria-label={`Character ${i + 1} of 6`} />
4 ))}
5</OTPField>

Alphanumeric

Use validationType to accept letters, digits, or both. Defaults to "numeric".

1<OTPField length={6} validationType="alphanumeric">
2 {Array.from({ length: 6 }, (_, i) => (
3 <OTPField.Input key={i} aria-label={`Character ${i + 1} of 6`} />
4 ))}
5</OTPField>

Disabled

Set disabled to prevent interaction.

1<OTPField length={6} disabled defaultValue="123">
2 {Array.from({ length: 6 }, (_, i) => (
3 <OTPField.Input key={i} aria-label={`Character ${i + 1} of 6`} />
4 ))}
5</OTPField>

Read-only

Set readOnly to display a value without allowing edits.

1<OTPField length={6} readOnly defaultValue="934821">
2 {Array.from({ length: 6 }, (_, i) => (
3 <OTPField.Input key={i} aria-label={`Character ${i + 1} of 6`} />
4 ))}
5</OTPField>

Controlled

Pass value and onValueChange to control the field from React state.

1(function ControlledOTP() {
2 const [value, setValue] = React.useState("");
3
4 return (
5 <Flex direction="column" gap={4} align="start">
6 <OTPField length={6} value={value} onValueChange={setValue}>
7 {Array.from({ length: 6 }, (_, i) => (
8 <OTPField.Input key={i} aria-label={`Character ${i + 1} of 6`} />
9 ))}
10 </OTPField>
11 <Text size="small">
12 Current value: <code>{value || "(empty)"}</code>
13 </Text>
14 </Flex>
15 );

Complete callback

onValueComplete fires once all slots are filled. Combine with autoSubmit to submit the surrounding form automatically.

1(function CompleteOTP() {
2 const [submitted, setSubmitted] = React.useState("");
3
4 return (
5 <Flex direction="column" gap={4} align="start">
6 <OTPField length={6} onValueComplete={(value) => setSubmitted(value)}>
7 {Array.from({ length: 6 }, (_, i) => (
8 <OTPField.Input key={i} aria-label={`Character ${i + 1} of 6`} />
9 ))}
10 </OTPField>
11 <Text size="small">
12 {submitted ? `Submitted: ${submitted}` : "Type all 6 digits to submit"}
13 </Text>
14 </Flex>
15 );

Custom sanitization

Set validationType="none" and provide sanitizeValue to restrict input to a custom set of characters.

1<OTPField
2 length={4}
3 validationType="none"
4 inputMode="numeric"
5 sanitizeValue={(val) => val.replace(/[^0-3]/g, "")}
6>
7 {Array.from({ length: 4 }, (_, i) => (
8 <OTPField.Input key={i} aria-label={`Character ${i + 1} of 4`} />
9 ))}
10</OTPField>

With Field

Compose with Field to get an associated label and description.

1<Flex justify="center">
2 <Field
3 label="Verification code"
4 description="Enter the 6-digit code we sent to your device."
5 >
6 <OTPField length={6}>
7 {Array.from({ length: 6 }, (_, i) => (
8 <OTPField.Input key={i} aria-label={`Character ${i + 1} of 6`} />
9 ))}
10 </OTPField>
11 </Field>
12</Flex>

Accessibility

  • Each slot must have an accessible name. Use a wrapping <label>, Field.Label, or set aria-label on every slot announcing its position (e.g., "Character 1 of 6").
  • The first slot inherits id; subsequent slots derive their ids as {id}-2, {id}-3, and so on.
  • A hidden validation input is rendered with autoComplete="one-time-code" so browsers can suggest codes received via SMS.
  • Typing a character advances focus to the next slot. Backspace clears the current slot and steps backwards. Arrow keys move between slots without changing the value.
  • Paste fills slots starting at the focused position, honoring the configured validationType and length.