Add this component

npx zentauri-ui add otp-input

Accessibility notes

Keyboard accessible controls with labels, focus-visible rings, and state attributes. Keep visible labels or aria-labels when copying snippets.

Dependency notes

Check Installation for shared peers. This component keeps styling in Tailwind classes and the --zui-* token contract.

Forms

OTP input for verification flows

OTPInput renders a segmented one-time code field for sign-in, two-factor authentication, payment approval, and invite-code flows. It keeps native inputs under the hood while handling auto-advance, paste fill, backspace behavior, hidden form value, and completion callbacks.

Workspace verification

Paste a 6 digit code or type one cell at a time.

Verified code

Examples

OTPInput handles the interaction contract for authentication and payment verification screens: segmented entry, paste fill, completion, and controlled state.


Payment approval

Enter the code sent to your authenticator.


Secure sign in

Waiting for a complete code.

OTP Input variants playground

Pick an appearance, size, and allowed characters to preview the OTP input live. Toggle Show output / Show code and the snippet updates to match the selected variant.


One-time code

Try typing or pasting into the first cell.

All appearances

Every shipped appearance token at a glance. Click any swatch to load it into the playground above.


Allowed characters: ALPHANUMERIC | Length: 8

Invite code

Try typing or pasting into the first cell.


State: ERROR | Completion: onComplete

One-time code

Try typing or pasting into the first cell.

The code has expired. Request a new one.

Otp Input API

Generated from the package prop types and variant definitions.

OTPInput

OTPInputProps

Variants

PropTypeDefault
appearance
amberaquabluecrimsoncyandefaultelectricerrorforestglassgreenindigo+21
default
size
lgmdsm
md

State

PropTypeDefault
defaultValuestring | undefinednone
onValueChange((value: string) => void) | undefinednone
valuestring | undefinednone

Behavior

PropTypeDefault
allowedCharactersOTPInputAllowedCharacters | undefinednone
autoFocusboolean | undefinednone
cellClassNamestring | undefinednone
disabledboolean | undefinednone
errorMessageReactNodenone
hintReactNodenone
labelReactNodenone
lengthnumber | undefinednone
maskboolean | undefinednone
namestring | undefinednone
onComplete((value: string) => void) | undefinednone
separatorEverynumber | undefinednone
Inherited HTML props
PropTypeDefault
classNamestring | undefinednone
idstring | undefinednone
onClickMouseEventHandler<HTMLDivElement> | undefinednone
styleCSSProperties | undefinednone
titlestring | undefinednone

OTPInputCellVariant

OTPInputCellVariantProps

Behavior

PropTypeDefault
appearance'default' | 'outline' | 'blue' | 'cyan' | 'green' | 'lime' | 'mint' | 'ocean' | 'sapphire' | 'lavender' | 'ruby' | 'red' | 'slate' | 'zinc' | 'stone' | 'royal' | 'electric' | 'forest' | 'sunset' | 'magenta' | 'crimson' | 'aqua' | 'plum' | 'pink' | 'orange' | 'indigo' | 'success' | 'warning' | 'error' | 'info' | 'violet' | 'glass' | 'amber' | null | undefinednone
size'md' | 'sm' | 'lg' | null | undefinednone
CSS variable overrides

OTP input CSS variables

Override these OTP input variables on :root, a theme selector, or a component wrapper.

214 variables

Pattern: --zui-<component>-<slot?>-<variant?>-<property>-<state?>-dark?

:root {
  --zui-otp-label-fg: oklch(20.8% 0.042 265.755);
  --zui-otp-hint-fg: oklch(55.4% 0.046 257.417);
  --zui-otp-bg: #ffffff;
  --zui-otp-fg: oklch(20.8% 0.042 265.755);
  --zui-otp-ring-offset-focus: #ffffff;
  --zui-otp-separator: #94a3b8;
  --zui-otp-error-fg: oklch(58.6% 0.253 17.585);
  --zui-otp-default-border: #cbd5e1;
  --zui-otp-default-border-focus: oklch(44.6% 0.043 257.281);
  --zui-otp-default-ring-focus: oklch(44.6% 0.043 257.281 / 0.25);
  --zui-otp-outline-border: #64748b;
  --zui-otp-outline-border-focus: oklch(54.6% 0.245 262.881);
  --zui-otp-outline-ring-focus: oklch(54.6% 0.245 262.881 / 0.28);
  --zui-otp-glass-border: #ffffff66;
  --zui-otp-glass-bg: #ffffffcc;
  --zui-otp-glass-border-focus: oklch(70.7% 0.165 254.624);
  --zui-otp-glass-ring-focus: oklch(70.7% 0.165 254.624 / 0.32);
  --zui-otp-success-border: oklch(69.6% 0.17 162.48 / 0.6);
  --zui-otp-success-border-focus: oklch(59.6% 0.145 163.225);
  --zui-otp-success-ring-focus: oklch(59.6% 0.145 163.225 / 0.28);
  --zui-otp-error-border: oklch(58.6% 0.253 17.585 / 0.7);
  --zui-otp-error-border-focus: oklch(58.6% 0.253 17.585);
  --zui-otp-error-ring-focus: oklch(58.6% 0.253 17.585 / 0.28);
  --zui-otp-warning-border: oklch(79.5% 0.184 86.047 / 0.7);
  --zui-otp-warning-border-focus: oklch(68.1% 0.162 75.834);
  --zui-otp-warning-ring-focus: oklch(68.1% 0.162 75.834 / 0.28);
  --zui-otp-info-border: oklch(62.3% 0.214 259.815 / 0.7);
  --zui-otp-info-border-focus: oklch(54.6% 0.245 262.881);
  --zui-otp-info-ring-focus: oklch(54.6% 0.245 262.881 / 0.28);
  --zui-otp-violet-border: oklch(60.6% 0.25 292.717 / 0.7);
  --zui-otp-violet-border-focus: oklch(54.1% 0.281 293.009);
  --zui-otp-violet-ring-focus: oklch(54.1% 0.281 293.009 / 0.28);
  --zui-otp-amber-border: oklch(76.9% 0.188 70.08 / 0.7);
  --zui-otp-amber-border-focus: oklch(66.6% 0.179 58.318);
  --zui-otp-amber-ring-focus: oklch(66.6% 0.179 58.318 / 0.28);
  --zui-otp-pink-border: oklch(65.6% 0.241 354.308 / 0.7);
  --zui-otp-pink-border-focus: oklch(59.2% 0.249 0.584);
  --zui-otp-pink-ring-focus: oklch(59.2% 0.249 0.584 / 0.28);
  --zui-otp-indigo-border: oklch(58.5% 0.233 277.117 / 0.7);
  --zui-otp-indigo-border-focus: oklch(51.1% 0.262 276.966);
  --zui-otp-indigo-ring-focus: oklch(51.1% 0.262 276.966 / 0.28);
  --zui-otp-orange-border: oklch(70.5% 0.213 47.604 / 0.7);
  --zui-otp-orange-border-focus: oklch(64.6% 0.222 41.116);
  --zui-otp-orange-ring-focus: oklch(64.6% 0.222 41.116 / 0.28);
  --zui-otp-blue-border: #2563eb;
  --zui-otp-blue-bg: #2563eb14;
  --zui-otp-blue-fg: #0f172a;
  --zui-otp-cyan-border: #0891b2;
  --zui-otp-cyan-bg: #0891b214;
  --zui-otp-cyan-fg: #0f172a;
  --zui-otp-green-border: #16a34a;
  --zui-otp-green-bg: #16a34a14;
  --zui-otp-green-fg: #0f172a;
  --zui-otp-lime-border: #65a30d;
  --zui-otp-lime-bg: #65a30d14;
  --zui-otp-lime-fg: #0f172a;
  --zui-otp-mint-border: #10b981;
  --zui-otp-mint-bg: #10b98114;
  --zui-otp-mint-fg: #0f172a;
  --zui-otp-ocean-border: #0284c7;
  --zui-otp-ocean-bg: #0284c714;
  --zui-otp-ocean-fg: #0f172a;
  --zui-otp-sapphire-border: #1d4ed8;
  --zui-otp-sapphire-bg: #1d4ed814;
  --zui-otp-sapphire-fg: #0f172a;
  --zui-otp-lavender-border: #8b5cf6;
  --zui-otp-lavender-bg: #8b5cf614;
  --zui-otp-lavender-fg: #0f172a;
  --zui-otp-ruby-border: #be123c;
  --zui-otp-ruby-bg: #be123c14;
  --zui-otp-ruby-fg: #0f172a;
  --zui-otp-red-border: #dc2626;
  --zui-otp-red-bg: #dc262614;
  --zui-otp-red-fg: #0f172a;
  --zui-otp-slate-border: #475569;
  --zui-otp-slate-bg: #47556914;
  --zui-otp-slate-fg: #0f172a;
  --zui-otp-zinc-border: #52525b;
  --zui-otp-zinc-bg: #52525b14;
  --zui-otp-zinc-fg: #0f172a;
  --zui-otp-stone-border: #57534e;
  --zui-otp-stone-bg: #57534e14;
  --zui-otp-stone-fg: #0f172a;
  --zui-otp-royal-border: #4338ca;
  --zui-otp-royal-bg: #4338ca14;
  --zui-otp-royal-fg: #0f172a;
  --zui-otp-electric-border: #0ea5e9;
  --zui-otp-electric-bg: #0ea5e914;
  --zui-otp-electric-fg: #0f172a;
  --zui-otp-forest-border: #166534;
  --zui-otp-forest-bg: #16653414;
  --zui-otp-forest-fg: #0f172a;
  --zui-otp-sunset-border: #ea580c;
  --zui-otp-sunset-bg: #ea580c14;
  --zui-otp-sunset-fg: #0f172a;
  --zui-otp-magenta-border: #c026d3;
  --zui-otp-magenta-bg: #c026d314;
  --zui-otp-magenta-fg: #0f172a;
  --zui-otp-crimson-border: #b91c1c;
  --zui-otp-crimson-bg: #b91c1c14;
  --zui-otp-crimson-fg: #0f172a;
  --zui-otp-aqua-border: #0f766e;
  --zui-otp-aqua-bg: #0f766e14;
  --zui-otp-aqua-fg: #0f172a;
  --zui-otp-plum-border: #7e22ce;
  --zui-otp-plum-bg: #7e22ce14;
  --zui-otp-plum-fg: #0f172a;
}

/* Dark theme variables follow the same names with -dark appended. */
.dark {
  --zui-otp-label-fg-dark: oklch(98.4% 0.003 247.858);
  --zui-otp-hint-fg-dark: oklch(70.4% 0.04 256.788);
}

What it does

Provide a purpose-built code entry primitive with individual cells that still behave like a single verification value.

The component sanitizes input, advances focus after entry, moves backward on Backspace, and fires onComplete when every cell is filled.

Composition and API

Import OTPInput from the otp-input entrypoint. Use length, allowedCharacters, separatorEvery, size, appearance, mask, name, value, defaultValue, onValueChange, and onComplete to tune the experience.

Common use cases

  • Two-factor authentication and passwordless sign-in.
  • Payment or wire-transfer approval codes.
  • Phone and email verification during onboarding.
  • Invite, recovery, and device pairing codes.

Accessibility

OTPInput renders a role=group wrapper with labelled native input cells. Provide label text or an accessible name, use hint for delivery instructions, and keep errorMessage concise so screen reader users hear the actionable state.

Next.js integration notes

Use OTPInput in client components when value changes in the browser. Static defaultValue examples can be rendered on preview pages, while verification calls should run from your own server action or API route after onComplete.

FAQ

Can OTPInput paste a full code?

Yes. Paste into any cell and OTPInput distributes valid characters across the remaining cells.

Does OTPInput support controlled state?

Yes. Use value with onValueChange for controlled state, or defaultValue for uncontrolled state.

Can OTPInput submit with a form?

Yes. Pass name to render a hidden input containing the full sanitized code.