You Might Also Like
Passkey Login — Free HTML CSS JS WebAuthn UI Snippet
Passkey Login · Forms · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
Passkey Login — Passwordless Sign-In UI with WebAuthn Detection and Email Fallback

Passwords are being replaced. Apple, Google, and Microsoft now ship passkeys — cryptographic credentials backed by Face ID, Touch ID, or Windows Hello — and modern login screens lead with a "Sign in with a passkey" button, keeping email as a fallback. This component is the front-end of that flow: a clean sign-in card with a primary passkey button that shows an authenticator-prompt loading state, a divider, an email field wired for passkey autofill, a fallback button, and a success toast. It is built in plain HTML, CSS, and vanilla JavaScript, and it feature-detects the real WebAuthn API so the demo behaves the way a production screen would.
The passkey-first layout
The card puts the passwordless path first: a fingerprint-style passkey button as the prominent dark primary action, an "or" divider, then the traditional email path below it as the secondary option. This ordering reflects the current best practice from the FIDO Alliance and the big platform vendors — surface the faster, phishing-resistant method, but never strand users who have not set up a passkey yet. The card has a gradient lock logo, a title, a subtitle that sets expectations ("no password to remember"), and a footer link to account creation.
Feature-detecting WebAuthn
When the passkey button is clicked, the script checks typeof window.PublicKeyCredential !== 'undefined' — the canonical way to detect whether the browser supports the Web Authentication API. In a real implementation this is the gate before calling navigator.credentials.get() with a server-issued challenge. Here it drives the demo's two outcomes: a "verified" toast when passkeys are supported, and a "not supported here" toast when they are not, so you can see both branches. This mirrors how you would progressively enhance a real login: show the passkey button only when the API exists, and fall back to email-and-password otherwise.
The authenticator-prompt loading state
Clicking the passkey button adds a .loading class to the card. Pure CSS then hides the button's icon and label and reveals a spinning SVG, communicating that the browser is waiting on the platform authenticator (the Face ID / Touch ID / Windows Hello system dialog). The button's pointer-events are disabled during this state so it cannot be double-triggered. A setTimeout simulates the 1.5-second authenticator round-trip; in production this period is exactly when navigator.credentials.get() is awaiting the user's biometric confirmation.
Passkey autofill on the email field
The email input carries autocomplete="username webauthn". That webauthn token is what enables "conditional UI" — browsers that support it will surface available passkeys directly in the email field's autofill dropdown, so a returning user can pick their passkey without even pressing the button. Including the token costs nothing on browsers that ignore it and unlocks the smoothest passkey experience on those that support it.
The success toast
Feedback is delivered through a pill toast pinned to the bottom of the card. It starts translated 80px down and transparent; adding a .show class slides it up and fades it in with a cubic-bezier spring, and a setTimeout hides it after 2.6 seconds. The toast has role="status" and aria-live="polite" so screen readers announce the result without stealing focus. The timer is cleared on each trigger so rapid clicks never leave it stuck.
Wiring to a real backend
To make this functional, replace the setTimeout with the real ceremony: fetch a challenge from your server, call navigator.credentials.get({ publicKey: { challenge, ... } }), and POST the resulting assertion back for verification. Keep the same UI states — add .loading before the call, remove it after, and show a success or error toast based on the response. Swap the #6366f1 / #0f172a palette and the logo for your brand, and edit the title and subtitle to match your product.
Build with AI
Build, Understand, Optimize, and Extend It With AI
You don't have to piece together the WebAuthn flow from scratch on your own. Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly what the window.PublicKeyCredential feature check is protecting against, and why the email input's autocomplete value of "username webauthn" specifically enables conditional UI passkey autofill in supporting browsers. The same assistant can help you optimize it, for example checking whether isUserVerifyingPlatformAuthenticatorAvailable should gate the button's visibility before the user even clicks it, or whether the loading and toast timers are cleared correctly under rapid repeat clicks. It's also useful for extending the effect: ask it to wire the setTimeout stub up to a real navigator.credentials.get() call against a server-issued challenge, add an error state for a rejected or cancelled biometric prompt, or add a "manage passkeys" list to account settings. 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 passwordless "sign in with a passkey" login card in plain HTML, CSS, and vanilla JavaScript that mirrors a real WebAuthn-based flow, with email as a fallback path — no frameworks, no backend required for the demo.
Requirements:
- A card with a primary "Sign in with a passkey" button as the dominant action, a visual divider, then a labeled email input and a secondary "Continue with email" button below it as the fallback path.
- The email input must carry autocomplete="username webauthn" so browsers that support conditional UI can surface saved passkeys directly in its autofill dropdown.
- On clicking the passkey button, feature-detect real WebAuthn support with a check like typeof window.PublicKeyCredential !== 'undefined', and branch the resulting message on whether it's supported.
- While the passkey ceremony is "in progress" (simulate this with a roughly 1.5 second delay standing in for navigator.credentials.get()), the button must enter a loading state driven by a single CSS class: hide its icon and label, show a spinning SVG icon, and disable further clicks via pointer-events.
- Show the result (success or unsupported) in an accessible toast pinned to the bottom of the card, using role="status" and aria-live="polite", animating in with a transform and opacity transition and auto-hiding after a few seconds, with any pending hide timer cleared on each new trigger so rapid re-clicks never leave it stuck.
- Document in a code comment exactly where a real implementation would fetch a challenge from the server and call navigator.credentials.get({ publicKey: { challenge, ... } }), replacing the setTimeout stub, while keeping the same loading and toast states around that async call.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 the HTML, CSS, and JSA sign-in card renders with a "Sign in with a passkey" button, an email fallback, and a footer link.
- 2Click the passkey buttonThe button enters a loading state — icon and label hide, a spinner appears — simulating the Face ID / Touch ID / Windows Hello prompt.
- 3See the result toastAfter ~1.5s a toast slides up confirming the passkey was verified, or noting passkeys are unsupported if the browser lacks WebAuthn.
- 4Try the email pathType an email and click "Continue with email" as the fallback; the field is tagged for passkey autofill (autocomplete="username webauthn").
- 5Connect a real backendReplace the setTimeout with a challenge fetch and navigator.credentials.get(), keeping the same loading and toast states.
- 6Theme itSwap the dark/indigo palette and the lock logo, and edit the title and subtitle to match your product.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
No — it is the front-end UI and flow only. The click handler feature-detects WebAuthn and simulates the authenticator prompt with a timeout so you can see the loading and success states. To make it functional, fetch a challenge from your server, call navigator.credentials.get({ publicKey: { challenge } }), and send the returned assertion back for verification, keeping the same .loading and toast states around that async call.
The webauthn token enables "conditional UI": browsers that support it surface the user's available passkeys directly in the email field's autofill dropdown, so a returning user can sign in by selecting a passkey without clicking the button. Browsers that do not support it simply treat the field as a normal username input, so the token is safe to include everywhere.
Wrap the button's display in the same check the handler uses: if (typeof window.PublicKeyCredential === 'undefined') hide the passkey button and show only the email path. For the best signal, also await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(), which confirms a built-in authenticator (Face ID, Touch ID, Windows Hello) is actually present before you promote the passkey option.
Not every user has set up a passkey, and not every browser or device supports them yet. Best practice is progressive enhancement: lead with passkeys where available, but always provide a working alternative (email link, OTP, or password) so nobody is locked out. The fallback button here represents that path.
Hold loading and toast booleans in state and bind them to the .loading and .show classes. Put the WebAuthn ceremony in an async click handler: set loading true, await navigator.credentials.get(), then set loading false and toggle the toast based on success. Clear the toast timeout in a cleanup (useEffect return / onUnmounted / ngOnDestroy). The card markup and all CSS states port unchanged — only the async flow and state move into the framework.