PIN Pad — Secure Keypad HTML CSS JS Snippet
PIN Pad · Forms · Plain HTML, CSS & JS · Live preview
What's included
Features
del removes the last digit and unfills its dot, on-screen (⌫) or via the Backspace key.keydown listener routes number keys and Backspace through the same pressKey/del as the pad.transition and the shake uses a transform keyframe, so feedback renders identically across frameworks.About this UI Snippet
PIN Pad — Filling Dots, Shake-on-Error & Unlock Success State

A PIN pad is the focused, secure-feeling way to take a short numeric code — lock screens, payment confirmation, parental gates, 2FA backup. Unlike a free-text field, it shows masked dots that fill as you type, validates automatically at the right length, and gives unmistakable feedback: a shake for wrong, a colour shift for correct. This snippet implements all of that in plain HTML, CSS, and vanilla JavaScript, with mouse, touch, and physical-keyboard input.
Masked dots that fill
Four dots represent the PIN. pressKey appends a digit to the pin string (capped at four) and render fills the corresponding dots — filled dots scale up slightly and take the accent colour. The actual digits are never shown, only the count, which is the privacy expectation for a PIN. Backspace (del) removes the last digit and unfills its dot.
Auto-submit and validation
There is no submit button — entering the fourth digit auto-checks after a short 180ms delay (so the final dot visibly fills before validation). check compares the pin to the correct value: a match locks the pad and switches to a success state (green dots, green lock icon, "✓ Unlocked"); a mismatch triggers feedback and clears.
Shake-on-error
A wrong PIN plays a horizontal pp-shake keyframe on the whole card — the universally understood "nope" gesture from iOS and macOS lock screens. It's re-triggered each time with the remove-class / force-reflow / add-class pattern so it fires on every failed attempt, the hint updates to "Wrong PIN — try again", and the entry clears after the shake so the user can retry. A locked flag blocks further input once unlocked.
Three input methods
The on-screen keypad is a clean 3×4 grid (1–9, blank, 0, backspace) with tactile press states. A keydown listener mirrors it so physical number keys and Backspace work too — every path routes through the same pressKey/del, so they can't diverge. The success/colour states use transition on colour and background, and the shake uses a transform keyframe — both export cleanly.
In production the check would call your backend (never compare a real PIN client-side), and you'd rate-limit attempts. Pair this with an OTP input for emailed codes, a pattern lock for gesture entry, or an auth login card for full sign-in.
Build with AI
Build, Understand, Optimize, and Extend It With AI
You don't have to trace the reflow trick or the auto-submit timing by hand. Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly why the shake animation on wrong entry needs the remove-class, read offsetWidth, re-add-class sequence to replay correctly, and why check runs on a short setTimeout after the fourth digit rather than immediately. The same assistant can help optimize it, for example checking whether the same pressKey and del logic could be shared more cleanly between the on-screen button clicks and the document keydown listener, or whether the locked flag correctly blocks every input path once the pad is unlocked. It's also useful for extending the effect: ask it to support a 6-digit PIN instead of 4, add a shuffled keypad layout to defeat shoulder-surfing, or wire the check function to a real backend call with rate-limiting instead of a hardcoded client-side comparison. Treat the code less like a finished artifact and more like a starting point for a conversation.
Prompt to recreate it
Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:
Build a numeric PIN entry pad in plain HTML, CSS, and vanilla JavaScript with masked progress dots, a shake-on-error animation, and a success state — no frameworks.
Requirements:
- A row of exactly four dot indicators that visually fill (change color and scale slightly) one at a time as digits are entered, without ever displaying the actual digits typed.
- A 3x4 on-screen keypad of buttons for digits 0 through 9 plus a delete/backspace button, where every button press appends or removes a digit from an in-memory PIN string capped at exactly 4 characters.
- The moment the fourth digit is entered, wait a short delay (so the final dot visibly finishes filling first) and then automatically compare the entered PIN against a stored correct value — there must be no explicit submit button.
- On a correct match, switch the whole card into a visually distinct success state (for example recoloring the dots, an icon, and the title text to a success color) and permanently disable further input until reset.
- On an incorrect match, replay a horizontal shake animation on the card every single time it fails (even on consecutive failures), using the standard remove-class, force a synchronous reflow by reading an element's offsetWidth, then re-add-class technique so the CSS animation restarts from frame zero instead of being skipped by the browser, then clear the entered PIN after the shake finishes so the user can retry.
- Mirror every on-screen keypad action with a document-level keydown listener so physical number keys and the Backspace key drive the exact same append/delete logic as the on-screen buttons, with no divergent code paths.Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.
Step by step
How to Use
- 1Paste HTML, CSS, and JSA dark PIN card appears with a lock icon, four empty dots, and a 3×4 numeric keypad.
- 2Enter the PINTap digits — each fills the next dot. Entering the fourth digit auto-validates after a brief beat.
- 3Try a wrong PINEnter anything but 1 9 3 7 — the card shakes, the hint says "Wrong PIN", and the dots clear so you can retry.
- 4Enter the correct PINType 1 9 3 7 — the dots and lock icon turn green and the title becomes "✓ Unlocked".
- 5Use backspaceTap ⌫ (or press Backspace) to remove the last digit before submitting.
- 6Type on your keyboardPhysical number keys and Backspace work too, routed through the same logic as the on-screen pad.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
Never compare the real PIN on the client. Send the entered PIN to your backend over HTTPS and verify it there (against a salted hash), returning success/failure. Keep the client-side check only for demos. Always rate-limit attempts server-side and lock the account or add a cooldown after several failures to prevent brute-forcing a 4-digit space.
Add two more .pp-dot elements and change the length checks from 4 to 6 in pressKey and the auto-submit condition. Everything else — fill rendering, shake, success — scales automatically since it's driven by pin.length and the number of dot elements.
The dots already mask digits. For stronger privacy, optionally shuffle the keypad layout each session (randomise the 0–9 positions) so observers can't infer the PIN from finger positions. For brute force, enforce server-side attempt limits and exponential backoff; the client locked flag is only a UX guard, not security.
The keys are real <button>s, so they're keyboard- and screen-reader operable, and a keydown handler adds physical number-key entry. Add an aria-label to each key, announce remaining digits and errors via an aria-live="polite" region (e.g. "3 of 4 entered", "incorrect PIN"), and ensure the success/error states are conveyed by text and icon, not colour alone.
In React, hold pin and a status ('idle' | 'ok' | 'error') in useState; pressKey appends and triggers validation in an effect when length hits 4, and the shake is a class keyed off status. In Vue, use refs and a watcher on pin.length. In Angular, track pin on the component. The dot-fill and shake CSS port unchanged.