Design.dev design.dev
UI Component Prompt

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.

Copy → Paste → Ship

Live Preview

Vanilla JS · Zero Dependencies

Enter verification code

Sent to [email protected]

 

Try pasting a 6-digit code anywhere in the row — the digits split across the cells. Valid code: 123456

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

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

Anatomy

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

Frequently Asked Questions

How do I build a 6-digit OTP / verification code input in HTML?

Render six text inputs in a row, each with type="text", inputmode="numeric", and maxlength="1". Listen to the input event on each cell: sanitize to a single digit, then move focus to the next empty cell. Handle Backspace to step back, paste to distribute six digits across cells, and the autocomplete="one-time-code" attribute so iOS, Safari, and Chrome can offer SMS codes inline. The prompt above outputs the full implementation.

Does it support iOS SMS auto-fill?

Yes. Each cell has autocomplete="one-time-code" set, which is the standard hint browsers and the iOS keyboard look for. When a verification SMS arrives that matches a known sender pattern, iOS surfaces the code as an inline keyboard suggestion that auto-distributes across the cells.

Why use type="text" inputmode="numeric" instead of type="number"?

type="number" allows scientific notation (e, +, -), shows browser spinners, and rejects leading zeros — all wrong for verification codes. type="text" with inputmode="numeric" keeps the mobile numpad keyboard while letting you sanitize input precisely (single-digit only, leading zeros allowed, no extra characters).

Can I change the number of digits to 4 or 8?

Yes. The prompt instructs the AI to expose a configurable length (default 6). Ask for a 4-digit PIN or an 8-digit code and the AI will adapt the markup, keyboard navigation, paste-distribution logic, and validation to the new length.

How do I validate the code on the server?

When all cells are filled the component assembles the digits into a single string and fires a completion callback. Send that string to your backend (e.g. POST /verify-otp) and switch the UI into a success or error state based on the JSON response. The included demo wires this against a hardcoded example so you can see the verified and error states immediately.

Is the OTP input keyboard accessible and screen-reader friendly?

Yes. The cell row is wrapped in a role="group" with an aria-label, each input has aria-label="Digit N", and the validation status uses role="status" aria-live="polite" so success and error messages are announced without stealing focus. The error shake respects prefers-reduced-motion.

Copied to clipboard