Skip to content
TopClass UI

OTP field

A one-time code typed one character per cell. A paste fills every cell, and one hidden input carries the code to the form.

client - size not measured yet

OTP field

client - size not measured yet

A one-time code typed one character per cell. A paste fills every cell, and one hidden input carries the code to the form.

Install

Try it

length
size
invalid
groupSize

The code

otp-field.tsx
import { OTPField } from 'topclassui/otp-field'

<div className="grid justify-items-start gap-2">
  <label htmlFor="otp-playground-code" className="text-sm text-body">Verification code</label>
  <OTPField id="otp-playground-code" />
</div>

The export is OTPField. In a form the hidden input sends what is typed so far, a partial code too.

Every state

6 states

Import from the package

topclassui/otp-field
import { OtpField } from 'topclassui/otp-field'

Install once:npm i topclassui@beta zod

Examples

  • length is 6 by default, clamped to a whole number from 1 to 12. groupSize draws a bar after every that many cells.
  • Digits are the default: ASCII, Arabic-Indic and Persian digits are taken and stored as ASCII, and spaces in a paste are dropped.
  • Past eight items, cells and bars together, the row breaks at whole cells, so twelve cells stay 44px tall on a phone.
  • Each cell is a square of the control scale, 40px at md, and gives up width before height. Six cells need 184px.
  • A Field's control object spreads onto it as it is.
basic

Six digits from your authenticator app

grouped
form
sizes
Extra small
Small
Medium
Large
Extra large
long
In Arabic, right to left

Props

The props of OtpField
PropTypeDefaultLimitsDescription
aria-labelledbystring--The id of the visible text that names the first cell.
idstring--The id of the first cell, which a <label htmlFor> points at; the others are <id>-2, <id>-3.
aria-describedbystring--On the first cell, beside its name: the id of a hint or an error text.
aria-invalidboolean | 'true' | 'false' | 'grammar' | 'spe...--On every cell; invalid sets it to true, and when invalid is off a Field's own value survives.
aria-labelundefined--Refused: Base UI drops it on the first cell. Name the field by id or aria-labelledby.
classNamestring--Merged last, so a consumer class wins a conflict with the group's own.
groupSizeinteger, min 0, max 120min 0, max 12Draws a separator after every this many cells; 0 draws none. Clamped between 0 and length.
invalidbooleanfalse-Marks the code as wrong: sets aria-invalid on every cell, whose line turns to danger and thickens.
lengthinteger, min 1, max 126min 1, max 12The number of cells, one character each. Clamped between 1 and 12; past eight items the row wraps.
refRef<HTMLDivElement>--The <div role="group"> around the cells.
sizexs | sm | md | lg | xl | custom"md"-The control scale of each square cell and its type step; custom emits no size classes.

Keyboard and accessibility

The keyboard of OtpField
KeysWhat it does
TabEnters the field: the group is one tab stop.
0-9Fills the cell and moves to the next one; a paste fills every cell from the focused one.
BackspaceClears the cell and moves back.
ArrowLeftArrowRightMove to the cell before or after, up to the first empty cell.
HomeEndMove to the first or the last cell the focus can reach.
  • Name it with id (for a <label htmlFor>) or aria-labelledby; the name lands on the first cell. aria-label is refused.
  • Every later cell is named by its place, "Character 2 of 6", in the provider's language. aria-describedby goes on the first cell.
  • invalid sets aria-invalid on every cell, never on the group: each line turns to danger and thickens.

Right to left

A code reads left to right, so on an Arabic page the cells still run left to right; the group sits at the inline start, beside its label.

Server and client

A client component: it renders on the server and hydrates in the browser.

Files: index.tsx, client.tsx.

On the chip: client - size not measured yet.