Zetta v1.6

Components · Forms & selection

OTP input

One-time password input composed of discrete character slots. Used for verification codes, PINs, and multi-factor authentication flows.

Preview

Build with the skill

No package to install — hand this to your AI to generate or migrate the component in your stack.

Prompt for your AI
Apply the Zetta "OTP input" component to this project using the zetta-design-md skill.

WHEN TO USE
- Migrating or fixing existing code (the common case): audit the current implementation against the `input-otp` spec and CORRECT every deviation — replace off-spec values, don't only add new ones.
- New component: build it to the `input-otp` spec in this project's stack.

AUDIT & FIX (when the component already exists)
Compare it to the skill's spec and fix, don't just append: hardcoded colors/hex, off-scale radius, wrong fonts, dark/navy fills, shadows used in place of borders (or missing borders), brand lime used as a CTA or interaction (must be primary indigo), and any missing or wrong hover / focus / active / disabled / invalid state or missing 2px focus ring. Replace each with the correct Zetta token.

ALWAYS
- Pull the authoritative spec and tokens from the skill — never guess or hardcode values.
- Use this project's own component conventions and framework idioms.
- Honor the Zetta guardrails (brand = identity only, primary = interaction, shadows for overlays) and verify in Light, Dark, and Accessibility.

Anatomy & rules

┌────┐ ┌────┐ ┌────┐  ─  ┌────┐ ┌────┐ ┌────┐
│  3 │ │  7 │ │  _ │     │    │ │    │ │    │
└────┘ └────┘ └────┘     └────┘ └────┘ └────┘
  slot   slot  slot(focus)  ↑    slot   slot   slot
                        separator

Parts:

  • input-otp-container — row flex wrapper, {spacing.sm} (8px) gap between slots
  • input-otp-slot — 32×32px box, Geist Mono 16px/500, centered text
  • input-otp-slot-focus — active slot, {colors.border-focus} border + 2px ring
  • input-otp-slot-filled — slot with a committed value, {colors.primary} border
  • input-otp-separator — 8×2px {colors.hairline} tick between slot groups
State Border Fill Notes
Default (empty) {colors.border-strong} {colors.surface-card}
Focus (active slot) {colors.border-focus} + 2px ring {colors.surface-card} Caret visible
Filled {colors.primary} {colors.surface-card} Character committed
Error {colors.danger} + danger-bg ring {colors.surface-card} All slots enter error state
Disabled {colors.disabled-border} {colors.disabled-bg} not-allowed, no focus

When to use

  • Email or phone verification (4–8 digit codes).
  • Multi-factor authentication (TOTP / SMS).
  • PIN entry flows.

When not to use

  • Long passwords — use Text Input with type="password".
  • Codes the user pastes rather than types — single-field paste is more accessible.

Do

  • Auto-advance focus to the next slot after each character.
  • Support paste into the first slot — distribute pasted characters across slots automatically.
  • Support Backspace to clear the current slot and return focus to the previous slot.
  • Fire onComplete only when all slots are filled — do not auto-submit without user confirmation.

Don't

  • Don't block paste — it is the primary input method for most users receiving codes.
  • Don't use OTP input for passwords or non-code text.
  • Don't show the separator on every slot — use it only to visually group digits (e.g. 3-3 for a 6-digit code).
  • Group label: "Verification code", "Enter PIN", "Authentication code".
  • Slot count should match the expected code length exactly — no padding slots.
  • Error message: "Incorrect code. Please try again." — not "Invalid OTP."
  • Resend link label: "Resend code" — pair below the OTP input with a countdown timer.
  • Separator: use only for grouping — e.g. groupSize={3} for a 6-digit SMS code.

Keyboard

Key Behavior
Tab Move focus to the first unfilled slot
0–9 / A–Z Enter character and advance focus to next slot
Backspace Clear current slot; if empty, clear previous slot and move focus back
Arrow Left Move focus to previous slot
Arrow Right Move focus to next slot
Paste Distribute pasted characters across slots starting from focused slot

Accessibility

  • The OTP container should be wrapped in a <fieldset> with a <legend> describing the code (e.g. "Enter the 6-digit verification code").
  • Each slot: role="textbox", aria-label="Digit N of M", inputmode="numeric" for numeric codes.
  • aria-invalid="true" on all slots when isInvalid is true.
  • aria-disabled="true" when disabled.
  • aria-live="polite" region to announce "Code entry complete" when onComplete fires.
  • Paste support is required — WCAG 1.3.5 mandates that verification codes must support clipboard paste.
  • Contrast: {colors.ink} on {colors.surface-card} is 21:1 (AAA). {colors.border-focus} ring is visible on all backgrounds.

Spec

Values and token references resolved straight from the Zetta spec.

input-otp-container
display
flex
flexDirection
row
gap
{spacing.sm}
justifyContent
center
alignItems
center
input-otp-slot
backgroundColor
{colors.surface-card}
textColor
{colors.ink}
borderColor
{colors.border-strong}
borderWidth
1px
borderRadius
{rounded.base}
fontFamily
Geist Mono
fontSize
16px
fontWeight
500
width
32px
height
32px
textAlign
center
input-otp-slot-focus
borderColor
{colors.border-focus}
outline
2px solid {colors.border-focus}
outlineOffset
2px
input-otp-slot-filled
borderColor
{colors.primary}
textColor
{colors.ink}
input-otp-slot-error
borderColor
{colors.danger}
outline
2px solid {colors.danger-bg}
outlineOffset
2px
input-otp-slot-disabled
backgroundColor
{colors.disabled-bg}
textColor
{colors.disabled-text}
borderColor
{colors.disabled-border}
cursor
not-allowed
input-otp-separator
color
{colors.hairline}
width
8px
height
2px