30 / 30
All 30 in 30 Tailwind CSS Login Forms
- 01Tailwind Split-Screen Brand Hero Login Form
- 02Tailwind Multi-Tenant Workspace Account Switcher
- 03Tailwind Floating Label Login Form with peer Variants
- 04Tailwind Web3 Wallet Connect Login Grid
- 05Tailwind Minimalist Fintech Login Card with Layered Shadow
- 06Tailwind OAuth Social Login Form with Brand SVG Buttons
- 07Tailwind Passwordless Magic Link Login with data-state Machine
- 08Tailwind Aurora Glow Login Card with Drifting Blur Blobs
- 09Tailwind Neon Synthwave Arcade Login Form
- 10Tailwind Sliding Login and Sign Up Panel with peer-checked
- 11Tailwind 3D Flip Login Card with Password Reset Back Face
- 12Tailwind Terminal CLI Prompt Login Form
- 13Tailwind Floating Pill Login Widget Docked Over App Content
- 14Tailwind Gradient Border Login Card with background-clip
- 15Tailwind Slide-Over Side Panel Login without JavaScript
- 16Tailwind Social-First Sign Up with Brand-Compliant Buttons
- 17Tailwind Animated Mascot Login that Covers Its Eyes
- 18Tailwind Accessible Compact Login Modal with Scale-In
- 19Tailwind Glassmorphism Login Card with backdrop-filter
- 20Tailwind Multi-Step Login Wizard with Radio-Driven Track
- 21Tailwind Animated Conic Gradient Border Login Card
- 22Tailwind Secure Vault Login with user-invalid Error State
- 23Tailwind Neo-Brutalist Login Form with Hard Offset Shadow
- 24Tailwind Show Hide Password Toggle with aria-pressed
- 25Tailwind OTP Code Input Grid with Auto-Advance
- 26Tailwind Password Strength Meter with data-score Variants
- 27Tailwind Numeric PIN Pad Login for Kiosks and POS
- 28Tailwind Inline Form Validation with user-valid and user-invalid
- 29Tailwind Particle Constellation Login over Canvas
- 30Tailwind Biometric Face ID Prompt with Pulse Ripples
Tailwind Biometric Face ID Prompt with Pulse Ripples
A PWA-style face / touch-ID prompt: a large biometric glyph in a circular target with concentric ripples signalling that the app is listening. The port's substantive point is not the animation — it is that the fallback link is mandatory, and the markup says so.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="tlf-30 w-full min-h-svh grid place-items-center bg-tlf30-stage font-tlf30-sans p-6">
<div class="w-[min(360px,100%)] p-9 text-center bg-tlf30-card border border-tlf30-edge rounded-[22px] text-tlf30-ink shadow-[0_30px_70px_-44px_black]">
<h1 class="text-[22px] font-bold">Confirm it’s you</h1>
<p class="mt-1.5 mb-7 text-[13.5px]/[1.5] text-tlf30-mut">Use Face ID or your fingerprint to sign in.</p>
<button type="button" aria-label="Authenticate with biometrics" class="relative grid place-items-center size-26 mx-auto mb-6 rounded-full border-0 cursor-pointer bg-tlf30-wash text-tlf30-acc transition-[background-color,scale] duration-150 hover:bg-tlf30-washhover active:scale-95 focus-visible:outline-3 focus-visible:outline-offset-4 focus-visible:outline-tlf30-ring motion-reduce:transition-colors motion-reduce:active:scale-100">
<span aria-hidden="true" class="absolute inset-0 rounded-full border-2 border-tlf30-acc opacity-0 animate-tlf30-pulse motion-reduce:animate-none motion-reduce:opacity-40 motion-reduce:-inset-1.5"></span>
<span aria-hidden="true" class="absolute inset-0 rounded-full border-2 border-tlf30-acc opacity-0 animate-tlf30-pulse [animation-delay:1.2s] motion-reduce:hidden"></span>
<svg viewBox="0 0 24 24" aria-hidden="true" class="relative size-13"><path fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" d="M5 11a7 7 0 0 1 14 0M7.5 12.5a4.5 4.5 0 0 1 9 0v1.5M12 12v4M9.5 15v1.5M14.5 14.5V17M4.5 14v1"></path></svg>
</button>
<p role="status" aria-live="polite" class="mb-5 text-[12.5px] text-tlf30-mut">Waiting for authentication…</p>
<a href="#" class="inline-grid place-items-center min-h-11 px-3 text-[13.5px] font-semibold text-tlf30-acc no-underline hover:underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tlf30-acc">Use passcode instead</a>
</div>
</section><section class="tlf-30 w-full min-h-svh grid place-items-center bg-tlf30-stage font-tlf30-sans p-6">
<div class="w-[min(360px,100%)] p-9 text-center bg-tlf30-card border border-tlf30-edge rounded-[22px] text-tlf30-ink shadow-[0_30px_70px_-44px_black]">
<h1 class="text-[22px] font-bold">Confirm it’s you</h1>
<p class="mt-1.5 mb-7 text-[13.5px]/[1.5] text-tlf30-mut">Use Face ID or your fingerprint to sign in.</p>
<button type="button" aria-label="Authenticate with biometrics" class="relative grid place-items-center size-26 mx-auto mb-6 rounded-full border-0 cursor-pointer bg-tlf30-wash text-tlf30-acc transition-[background-color,scale] duration-150 hover:bg-tlf30-washhover active:scale-95 focus-visible:outline-3 focus-visible:outline-offset-4 focus-visible:outline-tlf30-ring motion-reduce:transition-colors motion-reduce:active:scale-100">
<span aria-hidden="true" class="absolute inset-0 rounded-full border-2 border-tlf30-acc opacity-0 animate-tlf30-pulse motion-reduce:animate-none motion-reduce:opacity-40 motion-reduce:-inset-1.5"></span>
<span aria-hidden="true" class="absolute inset-0 rounded-full border-2 border-tlf30-acc opacity-0 animate-tlf30-pulse [animation-delay:1.2s] motion-reduce:hidden"></span>
<svg viewBox="0 0 24 24" aria-hidden="true" class="relative size-13"><path fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" d="M5 11a7 7 0 0 1 14 0M7.5 12.5a4.5 4.5 0 0 1 9 0v1.5M12 12v4M9.5 15v1.5M14.5 14.5V17M4.5 14v1"></path></svg>
</button>
<p role="status" aria-live="polite" class="mb-5 text-[12.5px] text-tlf30-mut">Waiting for authentication…</p>
<a href="#" class="inline-grid place-items-center min-h-11 px-3 text-[13.5px] font-semibold text-tlf30-acc no-underline hover:underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tlf30-acc">Use passcode instead</a>
</div>
</section>@import "tailwindcss";
@theme {
--color-tlf30-stage: #0f1220;
--color-tlf30-stage: oklch(0.17 0.025 275);
--color-tlf30-card: #171a2b;
--color-tlf30-card: oklch(0.22 0.025 275);
--color-tlf30-edge: #2b3048;
--color-tlf30-edge: oklch(0.29 0.028 275);
--color-tlf30-ink: #e9ecf7;
--color-tlf30-ink: oklch(0.94 0.01 275);
--color-tlf30-mut: #a6adc6;
--color-tlf30-mut: oklch(0.75 0.022 275);
--color-tlf30-acc: #8ba4f5;
--color-tlf30-acc: oklch(0.72 0.14 265);
--color-tlf30-wash: #232a4a;
--color-tlf30-wash: oklch(0.72 0.14 265 / 0.16);
--color-tlf30-washhover: #2c3660;
--color-tlf30-washhover: oklch(0.72 0.14 265 / 0.24);
--color-tlf30-ring: #adbef8;
--color-tlf30-ring: oklch(0.72 0.14 265 / 0.6);
--font-tlf30-sans: 'Sora', system-ui, sans-serif;
--animate-tlf30-pulse: tlf-30-pulse 2.4s ease-out infinite;
}
@keyframes tlf-30-pulse {
0% {
transform: scale(0.7);
opacity: 0.7;
}
80%,
100% {
transform: scale(1.5);
opacity: 0;
}
}
.tlf-30 {
width: 100%;
min-height: 100svh;
color-scheme: dark;
}@import "tailwindcss";
@theme {
--color-tlf30-stage: #0f1220;
--color-tlf30-stage: oklch(0.17 0.025 275);
--color-tlf30-card: #171a2b;
--color-tlf30-card: oklch(0.22 0.025 275);
--color-tlf30-edge: #2b3048;
--color-tlf30-edge: oklch(0.29 0.028 275);
--color-tlf30-ink: #e9ecf7;
--color-tlf30-ink: oklch(0.94 0.01 275);
--color-tlf30-mut: #a6adc6;
--color-tlf30-mut: oklch(0.75 0.022 275);
--color-tlf30-acc: #8ba4f5;
--color-tlf30-acc: oklch(0.72 0.14 265);
--color-tlf30-wash: #232a4a;
--color-tlf30-wash: oklch(0.72 0.14 265 / 0.16);
--color-tlf30-washhover: #2c3660;
--color-tlf30-washhover: oklch(0.72 0.14 265 / 0.24);
--color-tlf30-ring: #adbef8;
--color-tlf30-ring: oklch(0.72 0.14 265 / 0.6);
--font-tlf30-sans: 'Sora', system-ui, sans-serif;
--animate-tlf30-pulse: tlf-30-pulse 2.4s ease-out infinite;
}
@keyframes tlf-30-pulse {
0% {
transform: scale(0.7);
opacity: 0.7;
}
80%,
100% {
transform: scale(1.5);
opacity: 0;
}
}
.tlf-30 {
width: 100%;
min-height: 100svh;
color-scheme: dark;
}Setup — do this once per project
Skip this if your project already runs Tailwind v4.
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>Compiles in the browser — perfect for a CodePen or a quick test, not for production.
npm install tailwindcss @tailwindcss/vite
npm install tailwindcss @tailwindcss/vite// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})Any v4 setup works — Vite, PostCSS or the CLI. Nothing in this demo is bundler-specific.
@import "tailwindcss";
@theme {
--color-tlf30-stage: #0f1220;
--color-tlf30-stage: oklch(0.17 0.025 275);
--color-tlf30-card: #171a2b;
--color-tlf30-card: oklch(0.22 0.025 275);
--color-tlf30-edge: #2b3048;
--color-tlf30-edge: oklch(0.29 0.028 275);
--color-tlf30-ink: #e9ecf7;
--color-tlf30-ink: oklch(0.94 0.01 275);
--color-tlf30-mut: #a6adc6;
--color-tlf30-mut: oklch(0.75 0.022 275);
--color-tlf30-acc: #8ba4f5;
--color-tlf30-acc: oklch(0.72 0.14 265);
--color-tlf30-wash: #232a4a;
--color-tlf30-wash: oklch(0.72 0.14 265 / 0.16);
--color-tlf30-washhover: #2c3660;
--color-tlf30-washhover: oklch(0.72 0.14 265 / 0.24);
--color-tlf30-ring: #adbef8;
--color-tlf30-ring: oklch(0.72 0.14 265 / 0.6);
--font-tlf30-sans: 'Sora', system-ui, sans-serif;
--animate-tlf30-pulse: tlf-30-pulse 2.4s ease-out infinite;
}
@keyframes tlf-30-pulse {
0% {
transform: scale(0.7);
opacity: 0.7;
}
80%,
100% {
transform: scale(1.5);
opacity: 0;
}
}
.tlf-30 {
width: 100%;
min-height: 100svh;
color-scheme: dark;
}@import "tailwindcss";
@theme {
--color-tlf30-stage: #0f1220;
--color-tlf30-stage: oklch(0.17 0.025 275);
--color-tlf30-card: #171a2b;
--color-tlf30-card: oklch(0.22 0.025 275);
--color-tlf30-edge: #2b3048;
--color-tlf30-edge: oklch(0.29 0.028 275);
--color-tlf30-ink: #e9ecf7;
--color-tlf30-ink: oklch(0.94 0.01 275);
--color-tlf30-mut: #a6adc6;
--color-tlf30-mut: oklch(0.75 0.022 275);
--color-tlf30-acc: #8ba4f5;
--color-tlf30-acc: oklch(0.72 0.14 265);
--color-tlf30-wash: #232a4a;
--color-tlf30-wash: oklch(0.72 0.14 265 / 0.16);
--color-tlf30-washhover: #2c3660;
--color-tlf30-washhover: oklch(0.72 0.14 265 / 0.24);
--color-tlf30-ring: #adbef8;
--color-tlf30-ring: oklch(0.72 0.14 265 / 0.6);
--font-tlf30-sans: 'Sora', system-ui, sans-serif;
--animate-tlf30-pulse: tlf-30-pulse 2.4s ease-out infinite;
}
@keyframes tlf-30-pulse {
0% {
transform: scale(0.7);
opacity: 0.7;
}
80%,
100% {
transform: scale(1.5);
opacity: 0;
}
}
.tlf-30 {
width: 100%;
min-height: 100svh;
color-scheme: dark;
}<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@400;500;600;700&display=swap">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@400;500;600;700&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
[animation-delay:1.2s]is the whole reason two elements read as an endless ripple; an arbitrary property is the right home for it, because a delay tied to one animation's duration is not a design-scale value.- The
motion-reduce:trio on the first ring (no animation, 40% opacity, expanded inset) is a small piece of design work rather than a switch-off — the prompt still looks like it is listening. - The fallback link is the only element in the collection whose justification is entirely non-visual, and it gets a 44px target for the same reason.
- UI change from the CSS original: Sora replaces the system stack, the muted copy and status text lifted from
#9298b3/#7f86a3to a singleoklch(0.75 0.022 275)token (7.2:1 on the card, up from 3.9:1 on the status line), the accent brightened to0.72, and the fallback link became a 44px target instead of bare text.
Here's a working CSS demo from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Tailwind Biometric Face ID Prompt with Pulse Ripples
Source: https://codefronts.com/components/tailwind-css-login-forms/tailwind-biometric-face-id-prompt-pulse-ripples/
A PWA-style face / touch-ID prompt: a large biometric glyph in a circular target with concentric ripples signalling that the app is listening. The port's substantive point is not the animation — it is that the fallback link is mandatory, and the markup says so.
## HTML
```html
<section class="tlf-30 w-full min-h-svh grid place-items-center bg-tlf30-stage font-tlf30-sans p-6">
<div class="w-[min(360px,100%)] p-9 text-center bg-tlf30-card border border-tlf30-edge rounded-[22px] text-tlf30-ink shadow-[0_30px_70px_-44px_black]">
<h1 class="text-[22px] font-bold">Confirm it’s you</h1>
<p class="mt-1.5 mb-7 text-[13.5px]/[1.5] text-tlf30-mut">Use Face ID or your fingerprint to sign in.</p>
<button type="button" aria-label="Authenticate with biometrics" class="relative grid place-items-center size-26 mx-auto mb-6 rounded-full border-0 cursor-pointer bg-tlf30-wash text-tlf30-acc transition-[background-color,scale] duration-150 hover:bg-tlf30-washhover active:scale-95 focus-visible:outline-3 focus-visible:outline-offset-4 focus-visible:outline-tlf30-ring motion-reduce:transition-colors motion-reduce:active:scale-100">
<span aria-hidden="true" class="absolute inset-0 rounded-full border-2 border-tlf30-acc opacity-0 animate-tlf30-pulse motion-reduce:animate-none motion-reduce:opacity-40 motion-reduce:-inset-1.5"></span>
<span aria-hidden="true" class="absolute inset-0 rounded-full border-2 border-tlf30-acc opacity-0 animate-tlf30-pulse [animation-delay:1.2s] motion-reduce:hidden"></span>
<svg viewBox="0 0 24 24" aria-hidden="true" class="relative size-13"><path fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" d="M5 11a7 7 0 0 1 14 0M7.5 12.5a4.5 4.5 0 0 1 9 0v1.5M12 12v4M9.5 15v1.5M14.5 14.5V17M4.5 14v1"></path></svg>
</button>
<p role="status" aria-live="polite" class="mb-5 text-[12.5px] text-tlf30-mut">Waiting for authentication…</p>
<a href="#" class="inline-grid place-items-center min-h-11 px-3 text-[13.5px] font-semibold text-tlf30-acc no-underline hover:underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tlf30-acc">Use passcode instead</a>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tlf30-stage: #0f1220;
--color-tlf30-stage: oklch(0.17 0.025 275);
--color-tlf30-card: #171a2b;
--color-tlf30-card: oklch(0.22 0.025 275);
--color-tlf30-edge: #2b3048;
--color-tlf30-edge: oklch(0.29 0.028 275);
--color-tlf30-ink: #e9ecf7;
--color-tlf30-ink: oklch(0.94 0.01 275);
--color-tlf30-mut: #a6adc6;
--color-tlf30-mut: oklch(0.75 0.022 275);
--color-tlf30-acc: #8ba4f5;
--color-tlf30-acc: oklch(0.72 0.14 265);
--color-tlf30-wash: #232a4a;
--color-tlf30-wash: oklch(0.72 0.14 265 / 0.16);
--color-tlf30-washhover: #2c3660;
--color-tlf30-washhover: oklch(0.72 0.14 265 / 0.24);
--color-tlf30-ring: #adbef8;
--color-tlf30-ring: oklch(0.72 0.14 265 / 0.6);
--font-tlf30-sans: 'Sora', system-ui, sans-serif;
--animate-tlf30-pulse: tlf-30-pulse 2.4s ease-out infinite;
}
@keyframes tlf-30-pulse {
0% {
transform: scale(0.7);
opacity: 0.7;
}
80%,
100% {
transform: scale(1.5);
opacity: 0;
}
}
.tlf-30 {
width: 100%;
min-height: 100svh;
color-scheme: dark;
}
```Here's a working CSS demo from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Tailwind Biometric Face ID Prompt with Pulse Ripples
Source: https://codefronts.com/components/tailwind-css-login-forms/tailwind-biometric-face-id-prompt-pulse-ripples/
A PWA-style face / touch-ID prompt: a large biometric glyph in a circular target with concentric ripples signalling that the app is listening. The port's substantive point is not the animation — it is that the fallback link is mandatory, and the markup says so.
## HTML
```html
<section class="tlf-30 w-full min-h-svh grid place-items-center bg-tlf30-stage font-tlf30-sans p-6">
<div class="w-[min(360px,100%)] p-9 text-center bg-tlf30-card border border-tlf30-edge rounded-[22px] text-tlf30-ink shadow-[0_30px_70px_-44px_black]">
<h1 class="text-[22px] font-bold">Confirm it’s you</h1>
<p class="mt-1.5 mb-7 text-[13.5px]/[1.5] text-tlf30-mut">Use Face ID or your fingerprint to sign in.</p>
<button type="button" aria-label="Authenticate with biometrics" class="relative grid place-items-center size-26 mx-auto mb-6 rounded-full border-0 cursor-pointer bg-tlf30-wash text-tlf30-acc transition-[background-color,scale] duration-150 hover:bg-tlf30-washhover active:scale-95 focus-visible:outline-3 focus-visible:outline-offset-4 focus-visible:outline-tlf30-ring motion-reduce:transition-colors motion-reduce:active:scale-100">
<span aria-hidden="true" class="absolute inset-0 rounded-full border-2 border-tlf30-acc opacity-0 animate-tlf30-pulse motion-reduce:animate-none motion-reduce:opacity-40 motion-reduce:-inset-1.5"></span>
<span aria-hidden="true" class="absolute inset-0 rounded-full border-2 border-tlf30-acc opacity-0 animate-tlf30-pulse [animation-delay:1.2s] motion-reduce:hidden"></span>
<svg viewBox="0 0 24 24" aria-hidden="true" class="relative size-13"><path fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" d="M5 11a7 7 0 0 1 14 0M7.5 12.5a4.5 4.5 0 0 1 9 0v1.5M12 12v4M9.5 15v1.5M14.5 14.5V17M4.5 14v1"></path></svg>
</button>
<p role="status" aria-live="polite" class="mb-5 text-[12.5px] text-tlf30-mut">Waiting for authentication…</p>
<a href="#" class="inline-grid place-items-center min-h-11 px-3 text-[13.5px] font-semibold text-tlf30-acc no-underline hover:underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tlf30-acc">Use passcode instead</a>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tlf30-stage: #0f1220;
--color-tlf30-stage: oklch(0.17 0.025 275);
--color-tlf30-card: #171a2b;
--color-tlf30-card: oklch(0.22 0.025 275);
--color-tlf30-edge: #2b3048;
--color-tlf30-edge: oklch(0.29 0.028 275);
--color-tlf30-ink: #e9ecf7;
--color-tlf30-ink: oklch(0.94 0.01 275);
--color-tlf30-mut: #a6adc6;
--color-tlf30-mut: oklch(0.75 0.022 275);
--color-tlf30-acc: #8ba4f5;
--color-tlf30-acc: oklch(0.72 0.14 265);
--color-tlf30-wash: #232a4a;
--color-tlf30-wash: oklch(0.72 0.14 265 / 0.16);
--color-tlf30-washhover: #2c3660;
--color-tlf30-washhover: oklch(0.72 0.14 265 / 0.24);
--color-tlf30-ring: #adbef8;
--color-tlf30-ring: oklch(0.72 0.14 265 / 0.6);
--font-tlf30-sans: 'Sora', system-ui, sans-serif;
--animate-tlf30-pulse: tlf-30-pulse 2.4s ease-out infinite;
}
@keyframes tlf-30-pulse {
0% {
transform: scale(0.7);
opacity: 0.7;
}
80%,
100% {
transform: scale(1.5);
opacity: 0;
}
}
.tlf-30 {
width: 100%;
min-height: 100svh;
color-scheme: dark;
}
```How this works
Two rings, one animation, one delay. Both ripples scale from 0.7 to 1.5 while fading out over 2.4s; the second starts 1.2s in, which is what produces the continuous concentric effect from two elements instead of six.
The target is a real button. <button type="button"> with an aria-label, a 104px circle, and an active:scale-95 press. A decorative <div> here would make the primary action of the screen unreachable by keyboard.
Biometrics are never the only path. The passcode link is not a courtesy — a user with a failed sensor, a bandaged thumb or a borrowed device must be able to get in. It gets a 44px target and full contrast.
Make it yours
- Accent:
--color-tlf30-accdrives the target wash, the glyph, the ripples and the fallback link. - Ripple timing: the duration in
--animate-tlf30-pulseand the second ring's[animation-delay:1.2s](keep it at half). - Swap the fingerprint glyph for a Face ID mark — it is an inline SVG at
size-13. - Target size:
size-26on the button.
Gotchas — read before shipping
- Always ship the non-biometric fallback, and give it a real target size. This is the one requirement of the pattern that is not aesthetic.
- Under reduced motion, leave a static ring. Removing the ripples entirely makes the prompt look inert, which loses the "listening" signal the animation was carrying.
- Animate
scaleandopacityonly — a ripple that animateswidthrepaints the button's box every frame.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
@keyframes scale/opacity ripples and :focus-visible are universally supported; this is a UI shell, with no WebAuthn wired up.