OtpField

A React OTP input whose slots auto-advance and fill from a pasted code. For sign-in and phone verification.

OtpField playground

Two steps - sm for a dense form, md for the standard code cell.

Install OtpField

1. New project? One command installs the package and wires everything - see Installation:

bash
pnpm dlx zyncat-ui init

2. Import the component - it loads its own stylesheet:

tsx
import { OtpField } from '@zyncat/ui/otp-field';

Usage

tsx
import { OtpField } from '@zyncat/ui/otp-field';
 
<OtpField length={6} group={3} value={code} onChange={setCode} />

Compose OtpField in your application. No Tailwind or external styling library is required; every value resolves from Zyncat UI's token vocabulary.

OtpField props

OtpField

PropTypeDefaultDescription
...htmlAttributesHTMLAttributes—Standard <div> attributes (aria-*, data-*, ...) forwarded to the root wrapper.
lengthnumber6Number of slots.
valuestring—Controlled value (digit string).
onChange(value: string) => void—Called with the next value string.
groupnumber—Insert a separator every N slots (e.g. 3 - "-------").
errorboolean—Error state (red slots + ring).
disabledboolean—Disable every slot.
size'sm' | 'md''md'Slot width, height and digit type.
classNamestring—Extra class(es) appended to the root wrapper.
styleCSSProperties—Inline styles merged onto the root wrapper.

Frequently asked questions

Import OtpField from '@zyncat/ui/otp-field' and give it a value and an onChange: <OtpField length={6} group={3} value={code} onChange={setCode} />. length is the number of slots and group drops a separator every N of them, so group={3} renders 000-000. It ships compiled ESM with its 'use client' directive intact, so it drops straight into the Next.js App Router.

One monospaced slot per digit, wide enough to read at a glance and grouped so the code is scannable - OtpField draws 44x52px slots with a 10x2px dash between each group, or 38x44px under size="sm". The slot borders firm up as digits land, so the screen shows its own progress without a separate counter.

Yes. The paste handler strips everything that is not a digit and writes the rest across the slots from wherever you pasted, then leaves the caret where the code ran out - so pasting 492-013 straight out of a message fills all six and the dash is dropped for you. Typing works the same way one digit at a time: each entry advances to the next slot, and Backspace on an empty slot clears the one before it.

Every slot carries autocomplete="one-time-code", the attribute iOS and Safari look for before offering the code from a just-received SMS above the keyboard. Slots also set inputMode="numeric", so phones raise the number pad instead of the full keyboard, and non-digits are rejected on the way in.

Pass error. Every slot turns to the danger border, the focus ring swaps to the danger ring, and each input gets aria-invalid so a screen reader announces the failure rather than leaving it as colour only. Pair it with disabled while the code is in flight to the server.

Those are single-purpose packages you bolt onto a project. OtpField is one field inside a design system that already ships the labels, buttons and dialogs around it, and it adds no runtime dependencies of its own. Styling is CSS custom properties rather than Tailwind classes, so changing a slot is a token override instead of a className rewrite.

Set in Geist & Newsreader — animated by the house engineZyncat UI · Rev 0.11 · MIT · Built by Tabsir Ahammed · Source on GitHub