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.
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 slotsinput-otp-slot— 32×32px box,Geist Mono16px/500, centered textinput-otp-slot-focus— active slot,{colors.border-focus}border + 2px ringinput-otp-slot-filled— slot with a committed value,{colors.primary}borderinput-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
onCompleteonly 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 whenisInvalidis true.aria-disabled="true"when disabled.aria-live="polite"region to announce "Code entry complete" whenonCompletefires.- 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