Design.dev design.dev
Forms · Component Template

OTP / PIN Code Input

Six segmented fields with auto-advance, smart paste, backspace navigation, and a satisfying error shake. Copy the prompt, paste it, ship it.

View prompt

Live demo

Vanilla JS · Zero Dependencies

Type a digit to advance, paste a 6-digit code to fill all cells, backspace to step back. Wrong codes shake; the right one auto-submits.

The Prompt 659 words

Build a one-time-code (OTP / PIN) input using vanilla HTML, CSS, and JavaScript. No libraries, no frameworks — zero dependencies.

VISUAL DESIGN
- Six segmented input cells in a horizontal row with 10px gap between them
- Each cell: 48px wide, 56px tall, border-radius 10px, 1.5px border at rgba(255,255,255,0.1), background rgba(255,255,255,0.04)
- Centered 22px monospace-feel digit (use font-variant-numeric: tabular-nums for stable widths)
- Cyan caret color (#00c4ff)
- Hover state: brighter border and background
- Focus state: cyan border (#00c4ff), pale cyan background tint, 4px cyan glow ring
- Filled state: subtle cyan background tint and cyan-tinted border (so users see at a glance which cells are populated)
- Hide native number-input spinners (-webkit-appearance: none on inner/outer spin buttons; -moz-appearance: textfield)
- Status line below the row, min-height 18px so layout doesn't jump

VALIDATION STATES
- Success: all cells turn green (#00ff88) with green border and green tinted background, status text "Verified" in green
- Error: all cells turn red (#ff6b6b) with red border and red tinted background, status text "Invalid code, try again", and the row plays a 500ms shake keyframe (translate -2/4/-7/7/-7/4/-2)
- Cells lock to readOnly during success state to prevent further editing

INPUT BEHAVIOR
- type="text" with inputmode="numeric" so mobile keyboards show numpads (do not use type="number" — it allows scientific notation, "e", and signs)
- maxlength="1" per cell as a safety net, but ALSO sanitize the input event:
  - On input: take only the first numeric character of the new value, replace cell value with it
  - If the user typed a digit (cell was empty before, is now filled): focus the next empty cell
  - Non-numeric characters are silently rejected
- Auto-advance only on a real new character — pressing the same digit twice should still move forward
- Always select() on focus so the cell's existing value is overwritten by the next keystroke

KEYBOARD NAVIGATION
- ArrowLeft: move focus to previous cell (no wrap)
- ArrowRight: move focus to next cell (no wrap)
- Backspace on an empty cell: clear and focus the previous cell (the most expected behavior — never lose a keystroke)
- Backspace on a filled cell: clear it and stay (the next backspace then steps back per rule above)
- Delete: clear current cell, stay
- Home: focus first cell
- End: focus last filled cell (or last cell if all filled)
- Tab / Shift+Tab: leave the group entirely (do not trap focus inside)

PASTE HANDLING
- Listen for the paste event on the container (or each cell)
- Strip non-numeric characters from clipboard data, then take the first 6 digits
- Distribute one digit per cell starting from the focused cell (or from cell 0 if pasted into the container)
- After paste, focus the cell after the last filled cell (or stay on the last cell if the row is full)
- If paste fills all cells, fire the onComplete callback automatically

API
- Initialize: createOtpInput(container, { length: 6, onChange: (code) => ..., onComplete: (code) => ..., validate: (code) => boolean | Promise<boolean> })
- Public methods on the returned controller: getValue(), setValue(string), clear(), focus(), setError(message), setSuccess(message), reset()
- onComplete fires when all cells are filled; if validate is provided, it runs validate(code) and applies setSuccess or setError automatically

ACCESSIBILITY
- Wrap the row in role="group" with an aria-label like "One-time verification code"
- Each input has an aria-label "Digit 1" through "Digit N" so screen readers can identify the position
- Status message in role="status", aria-live="polite" — announced when validation result changes
- autocomplete="one-time-code" on each input so iOS / browsers can auto-fill from SMS
- prefers-reduced-motion: skip the shake animation; show error state instantly

OUTPUT
- Single HTML file with embedded <style> and <script>
- Demo with a "validate against hardcoded 123456" example
- Buttons to manually trigger valid / invalid / clear states for testing
- Polished, professional aesthetic — should feel like the verification screens on a premium modern sign-in flow
  1. CopyThe full prompt, one click.
  2. PasteInto Cursor, Claude Code, or any agent.
  3. CompareAgainst the live demo above.

What’s Inside

01

Segmented Cells

Six independent inputs in a row, each holding exactly one numeric character with stable tabular widths.

02

Auto-Advance

Typing a digit instantly forwards focus to the next empty cell — no extra clicks, no Tab presses.

03

Smart Paste

Pasting a 6-digit code anywhere in the row strips non-digits and distributes one character per cell.

04

Backspace Step-Back

Backspace on an empty cell jumps focus to the previous one and clears it — the gesture every user expects.

05

Filled vs. Empty

Filled cells get a subtle cyan tint so partial progress is obvious at a glance.

06

Error Shake

A 500ms horizontal shake plus red recolor signals invalid codes without disrupting the page layout.

07

SMS Auto-Fill

autocomplete="one-time-code" lets iOS, Safari, and Chrome offer SMS codes inline above the keyboard.

08

Live Status Region

An aria-live polite status line announces success or error to screen readers without stealing focus.

Usage Guidelines

Use This When

  • Verifying a one-time code from email or SMS for sign-in, password reset, or two-factor authentication
  • Confirming a payment, transaction, or destructive action with a short numeric PIN
  • Onboarding flows that need to confirm phone or email ownership before account creation

Not Ideal When

  • The code contains letters or symbols — use a single text input with monospace font instead
  • The code is longer than 8 characters — segmented cells become cramped and easy to misread
  • The user needs to memorize and type the same code repeatedly — a normal password field with a reveal toggle is friendlier
OTP / PIN Code Input
Copied to clipboard