OTP Field

A row of single character boxes for one-time passcodes and verification codes.

Enter the 6 digit code we sent to your phone.

import { Field, FieldDescription, FieldLabel } from "@hel/ui/field"
import { OTPField, OTPFieldInput } from "@hel/ui/otp-field"

When to use it

Use an OTP field to enter a short code that was sent to the person: a text message code, an email code, a recovery code or a PIN. Each character gets its own box, so the length is obvious and the cursor moves on by itself.

Typing, deleting and pasting all work the way people expect. A pasted code fills every box at once.

Import

import { OTPField, OTPFieldInput, OTPFieldSeparator } from "@hel/ui/otp-field"

Anatomy

OTPField is the row and takes the length. Put one OTPFieldInput inside it for each character, so a 6 character code has six inputs. Render them with a loop, as the example above does.

<OTPField length={6}>
  {Array.from({ length: 6 }, (_, index) => (
    <OTPFieldInput key={index} aria-label={`Digit ${index + 1} of 6`} />
  ))}
</OTPField>

Naming the inputs

A screen reader needs a name for the whole code and for each box. Inside a Field, the label names the first box. Give the others an aria-label like "Digit 2 of 6", and leave the first one without, as the example does. Outside a field, give the field an aria-label and every box one too.

Examples

Reacting to the finished code

onValueComplete is called once, with the whole code, when the last box is filled, which is the moment to check it. onValueChange fires on every change. Set autoSubmit to submit the surrounding form by itself when the code is complete.

Waiting for the code

import * as React from "react"

import { OTPField, OTPFieldInput } from "@hel/ui/otp-field"

Grouped boxes

Long codes are easier to read in chunks. Wrap each chunk in a Flex and put an OTPFieldSeparator between them. The boxes still belong to one field, so typing flows from one chunk to the next.

import { Flex } from "@hel/ui/flex"
import { OTPField, OTPFieldInput, OTPFieldSeparator } from "@hel/ui/otp-field"

Letters, numbers and hidden characters

The field accepts digits only by default. Set validationType="alphanumeric" for codes that mix letters and numbers. mask hides what is typed behind dots, which suits a PIN on a shared screen.

Recovery code, letters and numbers

Access PIN, hidden as you type

import { OTPField, OTPFieldInput } from "@hel/ui/otp-field"
import { Stack } from "@hel/ui/stack"
import { Text } from "@hel/ui/text"

Sizes

size on the field sizes every box: xs, sm, md, lg or xl, which is the default. They match the heights of other controls, so an OTP field lines up with buttons and inputs next to it. A single box can override it with its own size.

States

disabled and readOnly apply to every box, and required marks the code as needed. Inside an invalid Field, the boxes get the error ring.

Props

OTPField

PropTypeDefault
lengthnumberrequired
value / defaultValuestringnone
onValueChange(value: string) => voidnone
onValueComplete(value: string) => voidnone
validationType"numeric" | "alphanumeric""numeric"
maskbooleanfalse
autoSubmitbooleanfalse
size"xs" | "sm" | "md" | "lg" | "xl""xl"
disabled / readOnly / requiredbooleanfalse
namestringnone

It also takes every prop of the Base UI OTP field.

OTPFieldInput

One box. It takes size to override the field's, and every prop of an input, such as aria-label.

OTPFieldSeparator

A divider between chunks. It takes the props of a Separator.