More Forms Snippets
Pattern Lock HTML CSS JS — Android Gesture PIN
Pattern Lock · Forms · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
How to Build an Android-Style Pattern Lock on HTML5 Canvas

A pattern lock is the 3x3 grid of dots you connect with a single gesture to unlock an Android phone. This recreation draws the nine dots and the connecting path on an HTML5 Canvas, tracks the drag with the Pointer Events API, validates the traced sequence against a stored pattern, and adds real-world touches like an attempt counter, a lockout after too many failures, shake feedback and a two-step change-PIN flow. It is pure JavaScript and canvas — no libraries. Here is how it works.
Laying out and drawing the dots
The nine dots are positioned on a regular grid. Their center coordinates are computed from the canvas size, an outer padding and the spacing between columns and rows, then stored in an array of {x, y, index} objects. Each render pass clears the canvas and redraws every dot as a filled circle with ctx.arc, drawing a larger faint outer ring and a smaller solid inner core so selected dots can be highlighted by enlarging or recoloring the core. Keeping dot positions in an array makes both drawing and hit testing trivial.
Mapping pointer position to canvas space
All interaction uses Pointer Events so mouse, touch and stylus share one code path. On pointerdown the gesture begins; on pointermove the current path is extended; on pointerup the pattern is evaluated. Because the canvas can be displayed at a different size than its bitmap, every pointer event is converted to canvas-relative coordinates using getBoundingClientRect: subtract the rect's left/top from clientX/clientY and scale by the ratio of canvas pixel size to displayed size. This guarantees the gesture lines up with the dots on any screen.
Hit testing dots during the drag
As the pointer moves, the code checks whether it is near an unvisited dot. For each dot it measures the Euclidean distance from the pointer to the dot center, Math.hypot(px - dot.x, py - dot.y), and if that distance is within a generous threshold — roughly the dot radius plus a snap radius — and the dot is not already in the path, the dot's index is pushed onto the path array and the dot is marked selected. The snap radius makes the lock forgiving, so users do not have to hit dots precisely. Each newly captured dot is the next vertex of the unlock pattern.
Drawing the connecting path
On every move the canvas is redrawn: the dots first, then a polyline connecting the centers of all dots currently in path in order, then a final segment from the last captured dot to the live pointer position so the line appears to follow the finger. The stroke uses a rounded lineCap and lineJoin and a glowing color. This live trailing segment is what makes the gesture feel responsive and continuous.
Validating the pattern
When the pointer is released, the traced path array of dot indices is compared against the stored correct pattern. Validation is a simple element-by-element array comparison after a length check — the sequences must match exactly, since order is part of the secret. On success the path turns green and an unlocked state is shown. On failure the path turns red, a CSS shake animation is applied to the card by toggling a class, the attempt counter increments, and after a short setTimeout the canvas resets to the neutral state ready for another try.
Lockout after repeated failures
To mimic real device security, an attempt counter tracks consecutive failures. Once it reaches a limit, the lock enters a temporary lockout: input is disabled, a countdown message is shown, and a setTimeout re-enables input after a delay while resetting the counter. This demonstrates a basic brute-force mitigation pattern entirely on the client.
The change-PIN flow
Setting a new pattern is a deliberate two-step confirmation, exactly like a phone. The user first draws a candidate pattern, which is held in a temporary variable; the UI then prompts them to draw it again to confirm. If the second trace matches the first, the new pattern becomes the stored secret and the UI returns to the locked state; if they differ, an error is shown and the flow restarts. Requiring two matching traces prevents the user from locking themselves out with a mis-drawn pattern. A small state variable ('verify', 'set-first', 'set-confirm') drives which mode the release handler is in.
Why these techniques matter
The component is a compact demonstration of canvas hit testing with distance math, Pointer Events with proper coordinate mapping, building an ordered selection during a drag, array-equality validation, and timed UI states for feedback and lockout. The same patterns underpin signature pads, gesture games and any drag-to-connect interface. Everything is self-contained, themeable through a few constants, and works identically across mouse and touch.
Build with AI
Build, Understand, Optimize, and Extend It With AI
You don't have to work through the collision math and state machine by hand. Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly how the pointer-to-canvas coordinate mapping in toCanvas handles a canvas whose displayed size differs from its bitmap size, or how the changePinStep variable turns one release handler into a three-mode state machine. The same assistant can help optimize it, for example checking whether the getDotAt distance check could be simplified for a fixed 3x3 grid, or whether the lockout timer and pattern state interact safely if a user starts a new gesture mid-countdown. It's also useful for extending the effect: ask it to support a 4x4 or 5x5 dot grid, add a subtle haptic-style pulse on each captured dot, or persist the stored pattern to localStorage so it survives a page reload. 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 an Android-style 9-dot pattern lock in plain HTML, CSS, and vanilla JavaScript using the HTML5 Canvas 2D API and the Pointer Events API — no libraries, no touch-specific event handling separate from pointer events.
Requirements:
- Render a 3x3 grid of dots on a canvas, with each dot's center position computed from the canvas size and stored in an array of objects alongside an index and a visual state (idle, active, error, success).
- On pointerdown, hit-test the nine dots by Euclidean distance from the pointer to each dot's center; if the pointer is within a generous snap radius of an unvisited dot, start a gesture path with that dot's index and mark it visited.
- On pointermove during the drag, continue hit-testing for new unvisited dots to append to the path in order, and redraw every frame: the connecting polyline between all captured dots, plus one extra live segment from the last captured dot to the current pointer position so the line visibly follows the pointer.
- Convert every pointer event's clientX/clientY into canvas-space coordinates using getBoundingClientRect and the ratio between the canvas's pixel dimensions and its displayed CSS size, so the gesture lines up with the dots regardless of how the canvas is scaled on the page.
- On pointerup, compare the captured path array element-by-element against a stored correct pattern; require a minimum of 4 dots. On mismatch, flash the path and dots red with a CSS shake animation on the card, increment a wrong-attempt counter, and after 3 consecutive failures disable input entirely and run a 10-second countdown before resetting the counter.
- Add a "Change PIN" flow that requires the user to draw a new pattern twice in a row with matching results before it replaces the stored pattern, showing an error and returning to the normal unlock mode if the two draws don't match.Step by step
How to Use
- 1Trace the patternPress and drag across the dots in order to draw your unlock gesture in one stroke.
- 2Release to submitLift the pointer to validate the traced sequence against the stored pattern.
- 3Read the feedbackA green path confirms success; a red path with a shake means it did not match.
- 4Mind the attemptsAfter several wrong tries the lock temporarily disables input as a lockout.
- 5Change the PINUse change PIN, draw a new pattern, then redraw it to confirm and save.
- 6Tune the snapAdjust the dot and snap radius constants in the JS to make hits more or less forgiving.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
On each pointer move it measures the distance from the pointer to every dot center. If a dot is within the dot radius plus a snap radius and is not already in the path, its index is added to the gesture sequence.
Every move redraws the canvas: the connected dots, then a live segment from the last captured dot to the current pointer position. That trailing segment makes the path appear to track the finger continuously.
The traced array of dot indices is compared element by element to the stored pattern after a length check. Because order matters, the sequences must match exactly for the unlock to succeed.
An attempt counter increments on each wrong try. After it reaches a threshold the lock disables input, shows a countdown, and a timer re-enables it later while resetting the counter, mimicking brute-force protection.
A two-step confirmation ensures you can reliably reproduce the new pattern. The first trace is held temporarily and the second must match it before it is saved, preventing accidental lockout from a mis-drawn gesture.
Yes. Click JSX for React, Vue for a Vue 3 SFC, Angular for a standalone component, or Tailwind for utility classes. In React, attach pointer listeners in useEffect, keep the selected dot sequence in a ref during the drag, and commit it to state on pointerup to trigger validation.