Source Code

<div class="demo">
  <div class="phone">
    <div class="notch"></div>
    <div class="screen" id="screen">
      <div class="lock-top">
        <span class="lock-time">9:41</span>
        <span class="lock-date">Thursday, August 27</span>
      </div>

      <div class="lock-mid">
        <button class="scan-ring" id="scanBtn" aria-label="Scan fingerprint to unlock">
          <svg id="fpIcon" width="34" height="34" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><path d="M12 3a7 7 0 0 0-7 7c0 2 .5 3 1 4M12 3a7 7 0 0 1 7 7c0 3-1 5-1 5M9 21c-.5-1-1-3-1-5a4 4 0 0 1 8 0c0 1 0 2-.5 3M12 7a5 5 0 0 0-5 5c0 2 .5 3 1 4M12 7a5 5 0 0 1 5 5c0 1.5-.3 3-1 4.5M12 11a1.5 1.5 0 0 0-1.5 1.5c0 2 .8 3.5 1.5 4.5"/></svg>
          <svg class="progress-ring" width="96" height="96" viewBox="0 0 96 96">
            <circle cx="48" cy="48" r="44" fill="none" stroke="#334155" stroke-width="3" />
            <circle id="progressCircle" cx="48" cy="48" r="44" fill="none" stroke="#34d399" stroke-width="3" stroke-linecap="round" stroke-dasharray="276.5" stroke-dashoffset="276.5" transform="rotate(-90 48 48)" />
          </svg>
        </button>
        <p class="scan-status" id="scanStatus">Tap to scan fingerprint</p>
      </div>

      <div class="lock-bottom">
        <span id="attemptsNote"></span>
      </div>
    </div>
  </div>
</div>

Mobile Biometric Unlock Screen — Simulated Fingerprint Scan with Real Retry Logic

Mobile Biometric Unlock Screen · Mobile · Plain HTML, CSS & JS · Live preview

What's included

Features

Progress ring animation driven by real elapsed time via performance.now(), not a fixed per-frame increment
requestAnimationFrame-based animation loop stays accurate regardless of the device's actual refresh rate
Genuinely randomized scan failure (~25%) models real biometric sensor read-failure rates, not an always-succeeds demo
Persistent failed-attempt counter that survives across scan attempts and resets only on success
Escalating guidance message — attempt count, then a passcode-fallback suggestion after repeated failures
Distinct visual states (idle, scanning, success, fail) with a shake animation specifically on failure
Circumference computed once from the SVG circle's actual radius, reused consistently for every progress update
Realistic phone lock-screen chrome (notch, time, date) for a believable mockup context

About this UI Snippet

Mobile Biometric Unlock Screen — A Scan Sequence With a Real Outcome, Not Just an Icon

Screenshot of the Mobile Biometric Unlock Screen snippet rendered live

Most lock-screen mockups show a static fingerprint icon with no actual interaction behind it. This one implements the full scan sequence a real biometric unlock goes through: a timed progress ring fills in as the "scan" runs, then resolves to either a success state or — realistically, since real sensors do occasionally fail to get a clean read — a failure state that increments a genuine attempt counter and eventually suggests falling back to a passcode.

The progress ring is driven by elapsed time, not a fixed-step counter

runScan() records a performance.now() timestamp at the start and, inside a requestAnimationFrame loop, computes pct = elapsed / duration on every frame — deriving progress from actual elapsed wall-clock time rather than incrementing a counter by a fixed amount per frame. This keeps the ring's fill rate consistent regardless of the device's actual frame rate; a lower-refresh-rate device gets fewer, larger per-frame progress jumps, while a high-refresh-rate device gets more, smaller ones, but the *total* animation duration stays accurate either way.

Circumference-based stroke-dashoffset, computed once and reused

CIRCUMFERENCE = 2 * Math.PI * 44 is calculated once from the SVG circle's actual radius attribute, and every progress update sets strokeDashoffset as CIRCUMFERENCE * (1 - pct) — the same stroke-dasharray/dashoffset ring-fill technique used elsewhere in this library's loaders, applied here to represent scan progress specifically rather than a generic loading state.

A genuinely randomized failure path, not just a happy-path demo

finishScan() resolves with Math.random() > 0.25 — a real ~25% chance of failure on every scan attempt, deliberately modeling that biometric sensors don't have perfect read rates in practice. A failed scan triggers a distinct visual state (a red icon with a brief shake animation) and increments a persistent failCount that survives across scan attempts until a success resets it back to zero.

The attempt counter changes its own message as it climbs

Below three failed attempts, the note explicitly states how many tries remain before a passcode fallback would be required ("Attempt 2 of 3…"); at three or more, the message itself changes to suggest using a passcode instead — modeling the realistic security UX pattern where a device nudges toward a fallback authentication method after repeated biometric failures, rather than letting a user retry an unreliable scan indefinitely with no escalation.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Ask an AI assistant to explain why deriving animation progress from performance.now() and elapsed time is more robust than incrementing a counter on every requestAnimationFrame callback, especially on devices with inconsistent frame rates. It's also worth asking for a version that integrates the real WebAuthn API for actual biometric authentication instead of a randomized simulation, or one that adds a Face ID-style scanning animation variant alongside the fingerprint version.

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:

text
Build a mobile biometric unlock screen mockup in HTML, CSS and vanilla JavaScript, inside a phone-shaped frame, with a realistic simulated scan sequence — no external libraries.

Requirements:
- A phone frame with a notch, showing the current time and date, and a centered fingerprint icon inside a circular scan button.
- Tapping the scan button must start an animated circular progress ring around the icon that fills over roughly one second, computed from real elapsed time (e.g. via performance.now() inside a requestAnimationFrame loop) rather than a fixed per-frame increment, so the total duration stays accurate regardless of frame rate.
- Once the progress ring completes, resolve the scan to either a success or failure outcome using a genuinely randomized chance of failure (roughly 1 in 4 attempts), not an always-succeeds simulation.
- On success, show a distinct visual state (e.g. a green icon) and a success message, and reset any failed-attempt count back to zero.
- On failure, show a distinct visual state (e.g. a red icon with a brief shake animation) and a "not recognized, tap to try again" message, while incrementing a persistent failed-attempt counter that survives across multiple scan attempts.
- Once the failed-attempt counter reaches a threshold (e.g. 3), change the displayed message to suggest falling back to a passcode instead of continuing to retry the biometric scan.

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

  1. 1
    Tap the fingerprint icon to run a scanThe ring fills over about 1.1 seconds, then resolves to a success or failure state.
  2. 2
    Try scanning several timesAbout 1 in 4 attempts fails — watch the attempt counter and its message change as failures accumulate.
  3. 3
    Adjust the scan durationChange the duration constant (in milliseconds) inside runScan() to make the simulated scan faster or slower.
  4. 4
    Adjust the simulated failure rateChange the 0.25 threshold in finishScan() — a higher number means a lower failure rate, and vice versa.
  5. 5
    Change the passcode-fallback thresholdUpdate the failCount >= 3 check in finishScan() to require more or fewer failed attempts before suggesting a passcode.

Real-world uses

Common Use Cases

Auth Flow Mockups
Show off a biometric unlock flow, including its realistic failure and retry path, in a portfolio or app pitch.
PROTOTYPE
Security UX Prototyping
Prototype and test copy/messaging for a real biometric-to-passcode fallback flow before implementation.
Design System Reference
A concrete reference implementation of a time-based progress ring animation using requestAnimationFrame.
EDUCATION
Teaching requestAnimationFrame Timing
A clean example of driving animation progress from elapsed time rather than a naive frame counter.
Related: Live Caption Overlay
See the Live Caption Overlay for a related mobile pattern worth pairing with this one.
Related: Mobile Search Filters Screen
See the Mobile Search Filters Screen for a related mobile pattern worth pairing with this one.
Related: Mobile Voice Message Recording Screen
See the Mobile Voice Message Recording Screen for a related mobile pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

requestAnimationFrame doesn't guarantee a fixed interval between calls — it varies with the device's actual refresh rate and current load. Computing pct = elapsedTime / totalDuration keeps the animation's real-world duration accurate regardless of how many or how few frames actually get scheduled during that time.

No — finishScan() uses Math.random() > 0.25, giving each attempt roughly a 25% chance of failing, deliberately modeling the reality that biometric sensors occasionally fail to get a clean read, rather than presenting an unrealistic always-succeeds demo.

The failCount variable persists across attempts (resetting only on a successful scan) and the status message changes once it reaches 3 or more, suggesting the user fall back to a passcode instead of continuing to retry — mirroring how real devices handle repeated biometric failures.

It uses the standard SVG stroke-dasharray/stroke-dashoffset technique: the circle's circumference is computed once from its radius, and stroke-dashoffset is set to circumference * (1 - progress) on every animation frame, so the ring visually fills as progress increases from 0 to 1.

Yes — change Math.random() > 0.25 to always return true (e.g. replace it with true), though keeping some randomized failure is what makes the retry-counter and passcode-fallback messaging logic actually demonstrable.

No — this is a purely visual and interaction simulation intended for mockups and prototyping. A production implementation of real biometric authentication would use the WebAuthn API rather than a randomized JavaScript simulation like this one.