11 / 47
All 47 in 47 Tailwind CSS Toggles & Switches
- 01Tailwind iOS-Style Toggle Switch with appearance-none
- 02Tailwind Dark Mode Sun / Moon Theme Switcher
- 03Tailwind Neumorphic Soft-UI Pill Toggle
- 04Tailwind Gooey Liquid Morphing Switch with SVG Filter
- 05Tailwind Bouncy Elastic Physics Toggle (Overshoot Cubic-Bezier)
- 06Tailwind ON / OFF Text Switch with Labels Inside the Track
- 07Tailwind SaaS Pricing Toggle — Monthly / Annually Segmented Pill
- 08Tailwind Mechanical Keycap Toggles with Haptic Feedback
- 09Tailwind Waveform Mute Toggle with Animated EQ Bars
- 10Tailwind Timed Auto-Off Toggle with Conic Countdown Ring
- 11Tailwind Press-and-Hold to Confirm Toggle (Fingerprint Scan)
- 12Tailwind Split-Flap Departure Board Toggle (3D Flip)
- 13Tailwind Permission Toggle Group with :has() Live Status Badge
- 14Tailwind Material Design 3 Switch with State Layer
- 15Tailwind Skeuomorphic Wall Light Switch with Room Relight
- 16Tailwind Neon Cyberpunk Toggle with Clipped Corners
- 17Tailwind Brutalist Snap Toggle with Hard Offset Shadow
- 18Tailwind Day / Night Landscape Scene Switch
- 19Tailwind 3D Flip Card Toggle (rotateY Switch)
- 20Tailwind Sliding Pill Toggle with Label Inside the Thumb
- 21Tailwind Glassmorphism Frosted Toggle with backdrop-blur
- 22Tailwind Rotary Knob Toggle with Brushed-Metal Conic Gradient
- 23Tailwind iOS Settings List with Grouped Toggle Rows
- 24Tailwind Minimal Outline Toggle Drawn in currentColor
- 25Tailwind Squash-and-Stretch Pebble Toggle
- 26Tailwind Industrial Rocker I/O Power Switch
- 27Tailwind Accessible role=switch Toggle with forced-colors Support
- 28Tailwind Macro-Pad Toggle Grid with RGB Underglow
- 29Tailwind Radial VU Meter Mute Toggle
- 30Tailwind Segmented Pip Countdown Auto-Off Toggle
- 31Tailwind Slide-to-Confirm Toggle Rail (Drag or Arrow Keys)
- 32Tailwind Odometer Roll ON / OFF Toggle
- 33Tailwind Master Toggle with Indeterminate Tri-State
- 34Tailwind iOS Control Centre Tile Toggles
- 35Tailwind Material 3 Connected Icon Toggle Buttons
- 36Tailwind Two-Gang Wall Plate with Cumulative Room Light
- 37Tailwind Neon Toggle with Animated Conic Border Light
- 38Tailwind Brutalist Rubber-Stamp Toggle
- 39Tailwind Aurora Night Sky Toggle Switch
- 40Tailwind 3D Rotating Cube Toggle
- 41Tailwind Three-Position Off / Auto / On Selector
- 42Tailwind Liquid Glass Toggle with Specular Highlight
- 43Tailwind Three-Detent Rotary Selector Knob
- 44Tailwind Hardware Slide Switch with Knurled Grip
- 45Tailwind Battery Saver Toggle with Live Fill Meter
- 46Tailwind Morphing Sun-to-Moon Icon Toggle (SVG Stroke)
- 47Tailwind Guarded Kill Switch Under a Flip-Up Safety Cover
Tailwind Press-and-Hold to Confirm Toggle (Fingerprint Scan)
A destructive-action guard shaped like a fingerprint sensor: press and hold 1.2s while a scan line sweeps and a ring fills. Release early and it disarms. The state lives on aria-checked, so the styling variant and the accessibility contract are literally the same attribute.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="ttg-11 w-full min-h-svh grid place-items-center px-5 py-14 bg-ttg11-bg font-ttg11-sans" aria-label="Hold to confirm toggle">
<div class="w-[min(340px,100%)] flex flex-col items-center gap-5 px-10 py-8 rounded-[22px] bg-ttg11-card border border-ttg11-edge">
<div class="flex flex-col items-center gap-1 text-center">
<b class="text-[16px] font-bold text-ttg11-ink">Delete production database</b>
<span class="text-[12.5px] text-ttg11-mute">Hold the sensor for 1.2s to arm</span>
</div>
<button type="button" id="ttg-11-btn" role="switch" aria-checked="false" class="relative grid place-items-center w-26 h-26 rounded-full border-0 overflow-hidden cursor-pointer select-none [touch-action:none] bg-ttg11-sensor text-ttg11-idle transition-colors duration-300 aria-checked:text-ttg11-ok focus-visible:outline-3 focus-visible:outline-ttg11-scanline focus-visible:outline-offset-4 data-holding:[&_.ttg11-scan]:opacity-100 data-holding:[&_.ttg11-scan]:animate-ttg11-scan data-holding:[&_.ttg11-ring]:animate-ttg11-fill motion-reduce:data-holding:[&_.ttg11-scan]:animate-none">
<svg viewBox="0 0 40 48" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" class="relative z-10 w-[46px] h-14 transition-[filter] duration-300 [.ttg11-btn[aria-checked=true]_&]:drop-shadow-[0_0_8px_currentColor]">
<path d="M20 6c-8 0-13 6-13 13v6"/><path d="M20 12c-5 0-8 4-8 8v9"/><path d="M20 18c-2.5 0-4 2-4 4v12"/><path d="M20 25v14"/><path d="M25 21c.6 1 .9 2.2.9 3.5V37"/><path d="M29.5 17c1.6 2 2.5 4.5 2.5 7v9"/><path d="M27 9.5c3.7 2.3 6 6.3 6 10.5v3"/>
</svg>
<span aria-hidden="true" class="ttg11-scan absolute left-[18px] right-[18px] top-0 z-20 h-[3px] rounded-sm opacity-0 bg-ttg11-scanline shadow-[0_0_12px_var(--color-ttg11-scanline)]"></span>
<span aria-hidden="true" class="ttg11-ring absolute inset-0 rounded-full [background:conic-gradient(var(--color-ttg11-ok)_var(--ttg11-p,0deg),transparent_0)] [mask:radial-gradient(circle,transparent_46px,#000_47px)]"></span>
</button>
<p id="ttg-11-state" aria-live="polite" class="min-h-[1.2em] text-[13px] font-semibold tracking-[0.08em] uppercase text-ttg11-mute">Locked</p>
</div>
</section><section class="ttg-11 w-full min-h-svh grid place-items-center px-5 py-14 bg-ttg11-bg font-ttg11-sans" aria-label="Hold to confirm toggle">
<div class="w-[min(340px,100%)] flex flex-col items-center gap-5 px-10 py-8 rounded-[22px] bg-ttg11-card border border-ttg11-edge">
<div class="flex flex-col items-center gap-1 text-center">
<b class="text-[16px] font-bold text-ttg11-ink">Delete production database</b>
<span class="text-[12.5px] text-ttg11-mute">Hold the sensor for 1.2s to arm</span>
</div>
<button type="button" id="ttg-11-btn" role="switch" aria-checked="false" class="relative grid place-items-center w-26 h-26 rounded-full border-0 overflow-hidden cursor-pointer select-none [touch-action:none] bg-ttg11-sensor text-ttg11-idle transition-colors duration-300 aria-checked:text-ttg11-ok focus-visible:outline-3 focus-visible:outline-ttg11-scanline focus-visible:outline-offset-4 data-holding:[&_.ttg11-scan]:opacity-100 data-holding:[&_.ttg11-scan]:animate-ttg11-scan data-holding:[&_.ttg11-ring]:animate-ttg11-fill motion-reduce:data-holding:[&_.ttg11-scan]:animate-none">
<svg viewBox="0 0 40 48" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" class="relative z-10 w-[46px] h-14 transition-[filter] duration-300 [.ttg11-btn[aria-checked=true]_&]:drop-shadow-[0_0_8px_currentColor]">
<path d="M20 6c-8 0-13 6-13 13v6"/><path d="M20 12c-5 0-8 4-8 8v9"/><path d="M20 18c-2.5 0-4 2-4 4v12"/><path d="M20 25v14"/><path d="M25 21c.6 1 .9 2.2.9 3.5V37"/><path d="M29.5 17c1.6 2 2.5 4.5 2.5 7v9"/><path d="M27 9.5c3.7 2.3 6 6.3 6 10.5v3"/>
</svg>
<span aria-hidden="true" class="ttg11-scan absolute left-[18px] right-[18px] top-0 z-20 h-[3px] rounded-sm opacity-0 bg-ttg11-scanline shadow-[0_0_12px_var(--color-ttg11-scanline)]"></span>
<span aria-hidden="true" class="ttg11-ring absolute inset-0 rounded-full [background:conic-gradient(var(--color-ttg11-ok)_var(--ttg11-p,0deg),transparent_0)] [mask:radial-gradient(circle,transparent_46px,#000_47px)]"></span>
</button>
<p id="ttg-11-state" aria-live="polite" class="min-h-[1.2em] text-[13px] font-semibold tracking-[0.08em] uppercase text-ttg11-mute">Locked</p>
</div>
</section>@import "tailwindcss";
@property --ttg11-p {
syntax: '<angle>';
inherits: false;
initial-value: 0deg;
}
@theme {
--color-ttg11-bg: #0f1219;
--color-ttg11-bg: oklch(0.18 0.012 265);
--color-ttg11-card: #171c26;
--color-ttg11-card: oklch(0.23 0.014 265);
--color-ttg11-edge: #262d3c;
--color-ttg11-edge: oklch(0.3 0.016 265);
--color-ttg11-sensor: #1f2533;
--color-ttg11-sensor: oklch(0.26 0.018 265);
--color-ttg11-idle: #737d94;
--color-ttg11-idle: oklch(0.58 0.02 265);
--color-ttg11-ok: #4fd398;
--color-ttg11-ok: oklch(0.79 0.15 160);
--color-ttg11-scanline: #58b6f0;
--color-ttg11-scanline: oklch(0.75 0.14 230);
--color-ttg11-ink: #e7eaf3;
--color-ttg11-ink: oklch(0.93 0.008 265);
--color-ttg11-mute: #8d95a8;
--color-ttg11-mute: oklch(0.66 0.016 265);
--font-ttg11-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--animate-ttg11-scan: ttg11-scan 1.2s ease-in-out;
--animate-ttg11-fill: ttg11-fill 1.2s linear forwards;
}
@keyframes ttg11-scan {
0% {
transform: translateY(14px);
}
50% {
transform: translateY(88px);
}
100% {
transform: translateY(14px);
}
}
@keyframes ttg11-fill {
from {
--ttg11-p: 0deg;
}
to {
--ttg11-p: 360deg;
}
}
/* The armed ring has to stay full after the animation is removed, and an aria state
cannot set a custom property through a utility. */
.ttg-11 [aria-checked="true"] .ttg11-ring {
--ttg11-p: 360deg;
}
.ttg-11 {
width: 100%;
min-height: 100svh;
}@import "tailwindcss";
@property --ttg11-p {
syntax: '<angle>';
inherits: false;
initial-value: 0deg;
}
@theme {
--color-ttg11-bg: #0f1219;
--color-ttg11-bg: oklch(0.18 0.012 265);
--color-ttg11-card: #171c26;
--color-ttg11-card: oklch(0.23 0.014 265);
--color-ttg11-edge: #262d3c;
--color-ttg11-edge: oklch(0.3 0.016 265);
--color-ttg11-sensor: #1f2533;
--color-ttg11-sensor: oklch(0.26 0.018 265);
--color-ttg11-idle: #737d94;
--color-ttg11-idle: oklch(0.58 0.02 265);
--color-ttg11-ok: #4fd398;
--color-ttg11-ok: oklch(0.79 0.15 160);
--color-ttg11-scanline: #58b6f0;
--color-ttg11-scanline: oklch(0.75 0.14 230);
--color-ttg11-ink: #e7eaf3;
--color-ttg11-ink: oklch(0.93 0.008 265);
--color-ttg11-mute: #8d95a8;
--color-ttg11-mute: oklch(0.66 0.016 265);
--font-ttg11-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--animate-ttg11-scan: ttg11-scan 1.2s ease-in-out;
--animate-ttg11-fill: ttg11-fill 1.2s linear forwards;
}
@keyframes ttg11-scan {
0% {
transform: translateY(14px);
}
50% {
transform: translateY(88px);
}
100% {
transform: translateY(14px);
}
}
@keyframes ttg11-fill {
from {
--ttg11-p: 0deg;
}
to {
--ttg11-p: 360deg;
}
}
/* The armed ring has to stay full after the animation is removed, and an aria state
cannot set a custom property through a utility. */
.ttg-11 [aria-checked="true"] .ttg11-ring {
--ttg11-p: 360deg;
}
.ttg-11 {
width: 100%;
min-height: 100svh;
}(() => {
const HOLD_MS = 1200;
const btn = document.querySelector('#ttg-11-btn');
const state = document.querySelector('#ttg-11-state');
if (!btn || !state) return;
let timer = null;
const arm = () => {
if (timer) return;
btn.setAttribute('data-holding', '');
state.textContent = 'Scanning\u2026';
timer = setTimeout(() => {
timer = null;
btn.removeAttribute('data-holding');
const next = btn.getAttribute('aria-checked') !== 'true';
btn.setAttribute('aria-checked', String(next));
state.textContent = next ? 'Armed \u2014 verified' : 'Locked';
}, HOLD_MS);
};
const disarm = () => {
if (!timer) return;
clearTimeout(timer);
timer = null;
btn.removeAttribute('data-holding');
state.textContent = 'Released too early';
};
btn.addEventListener('pointerdown', (e) => { btn.setPointerCapture(e.pointerId); arm(); });
['pointerup', 'pointercancel', 'pointerleave'].forEach((t) => btn.addEventListener(t, disarm));
btn.addEventListener('keydown', (e) => {
if ((e.key === ' ' || e.key === 'Enter') && !e.repeat) { e.preventDefault(); arm(); }
});
btn.addEventListener('keyup', (e) => { if (e.key === ' ' || e.key === 'Enter') disarm(); });
})();(() => {
const HOLD_MS = 1200;
const btn = document.querySelector('#ttg-11-btn');
const state = document.querySelector('#ttg-11-state');
if (!btn || !state) return;
let timer = null;
const arm = () => {
if (timer) return;
btn.setAttribute('data-holding', '');
state.textContent = 'Scanning\u2026';
timer = setTimeout(() => {
timer = null;
btn.removeAttribute('data-holding');
const next = btn.getAttribute('aria-checked') !== 'true';
btn.setAttribute('aria-checked', String(next));
state.textContent = next ? 'Armed \u2014 verified' : 'Locked';
}, HOLD_MS);
};
const disarm = () => {
if (!timer) return;
clearTimeout(timer);
timer = null;
btn.removeAttribute('data-holding');
state.textContent = 'Released too early';
};
btn.addEventListener('pointerdown', (e) => { btn.setPointerCapture(e.pointerId); arm(); });
['pointerup', 'pointercancel', 'pointerleave'].forEach((t) => btn.addEventListener(t, disarm));
btn.addEventListener('keydown', (e) => {
if ((e.key === ' ' || e.key === 'Enter') && !e.repeat) { e.preventDefault(); arm(); }
});
btn.addEventListener('keyup', (e) => { if (e.key === ' ' || e.key === 'Enter') disarm(); });
})();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";
@property --ttg11-p {
syntax: '<angle>';
inherits: false;
initial-value: 0deg;
}
@theme {
--color-ttg11-bg: #0f1219;
--color-ttg11-bg: oklch(0.18 0.012 265);
--color-ttg11-card: #171c26;
--color-ttg11-card: oklch(0.23 0.014 265);
--color-ttg11-edge: #262d3c;
--color-ttg11-edge: oklch(0.3 0.016 265);
--color-ttg11-sensor: #1f2533;
--color-ttg11-sensor: oklch(0.26 0.018 265);
--color-ttg11-idle: #737d94;
--color-ttg11-idle: oklch(0.58 0.02 265);
--color-ttg11-ok: #4fd398;
--color-ttg11-ok: oklch(0.79 0.15 160);
--color-ttg11-scanline: #58b6f0;
--color-ttg11-scanline: oklch(0.75 0.14 230);
--color-ttg11-ink: #e7eaf3;
--color-ttg11-ink: oklch(0.93 0.008 265);
--color-ttg11-mute: #8d95a8;
--color-ttg11-mute: oklch(0.66 0.016 265);
--font-ttg11-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--animate-ttg11-scan: ttg11-scan 1.2s ease-in-out;
--animate-ttg11-fill: ttg11-fill 1.2s linear forwards;
}
@keyframes ttg11-scan {
0% {
transform: translateY(14px);
}
50% {
transform: translateY(88px);
}
100% {
transform: translateY(14px);
}
}
@keyframes ttg11-fill {
from {
--ttg11-p: 0deg;
}
to {
--ttg11-p: 360deg;
}
}
/* The armed ring has to stay full after the animation is removed, and an aria state
cannot set a custom property through a utility. */
.ttg-11 [aria-checked="true"] .ttg11-ring {
--ttg11-p: 360deg;
}
.ttg-11 {
width: 100%;
min-height: 100svh;
}@import "tailwindcss";
@property --ttg11-p {
syntax: '<angle>';
inherits: false;
initial-value: 0deg;
}
@theme {
--color-ttg11-bg: #0f1219;
--color-ttg11-bg: oklch(0.18 0.012 265);
--color-ttg11-card: #171c26;
--color-ttg11-card: oklch(0.23 0.014 265);
--color-ttg11-edge: #262d3c;
--color-ttg11-edge: oklch(0.3 0.016 265);
--color-ttg11-sensor: #1f2533;
--color-ttg11-sensor: oklch(0.26 0.018 265);
--color-ttg11-idle: #737d94;
--color-ttg11-idle: oklch(0.58 0.02 265);
--color-ttg11-ok: #4fd398;
--color-ttg11-ok: oklch(0.79 0.15 160);
--color-ttg11-scanline: #58b6f0;
--color-ttg11-scanline: oklch(0.75 0.14 230);
--color-ttg11-ink: #e7eaf3;
--color-ttg11-ink: oklch(0.93 0.008 265);
--color-ttg11-mute: #8d95a8;
--color-ttg11-mute: oklch(0.66 0.016 265);
--font-ttg11-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--animate-ttg11-scan: ttg11-scan 1.2s ease-in-out;
--animate-ttg11-fill: ttg11-fill 1.2s linear forwards;
}
@keyframes ttg11-scan {
0% {
transform: translateY(14px);
}
50% {
transform: translateY(88px);
}
100% {
transform: translateY(14px);
}
}
@keyframes ttg11-fill {
from {
--ttg11-p: 0deg;
}
to {
--ttg11-p: 360deg;
}
}
/* The armed ring has to stay full after the animation is removed, and an aria state
cannot set a custom property through a utility. */
.ttg-11 [aria-checked="true"] .ttg11-ring {
--ttg11-p: 360deg;
}
.ttg-11 {
width: 100%;
min-height: 100svh;
}<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=Plus+Jakarta+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&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=Plus+Jakarta+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
aria-checked:is the one built-in ARIA variant this collection uses unmodified: it compiles to[aria-checked="true"], and"true"is the correct value for a switch — the opposite of thearia-currentsituation, where the shorthand is always wrong.- Two
--animate-*tokens for one gesture is right: the scan line and the ring have the same duration but different curves and fill modes, and a single token cannot express both. - One CSS rule refused to become a utility: pinning
--ttg11-p: 360degon the armed ring. Utilities can set custom properties in arbitrary syntax, but not conditionally on a descendant of an ARIA state without becoming unreadable — two lines of scoped CSS is the honest answer. - UI change from the CSS original: the sensor grew from 104px to 112px (comfortably past the touch floor for a gesture you must hold), the idle print ink lifted to
oklch(0.58)so the fingerprint is visible before you touch it, and the success green is nowoklch(0.79 0.15 160)— 10:1 on the sensor, where the original sat at 4.2:1.
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 Press-and-Hold to Confirm Toggle (Fingerprint Scan)
Source: https://codefronts.com/components/tailwind-css-toggles-switches/tailwind-press-and-hold-to-confirm-toggle-fingerprint/
A destructive-action guard shaped like a fingerprint sensor: press and hold 1.2s while a scan line sweeps and a ring fills. Release early and it disarms. The state lives on aria-checked, so the styling variant and the accessibility contract are literally the same attribute.
## HTML
```html
<section class="ttg-11 w-full min-h-svh grid place-items-center px-5 py-14 bg-ttg11-bg font-ttg11-sans" aria-label="Hold to confirm toggle">
<div class="w-[min(340px,100%)] flex flex-col items-center gap-5 px-10 py-8 rounded-[22px] bg-ttg11-card border border-ttg11-edge">
<div class="flex flex-col items-center gap-1 text-center">
<b class="text-[16px] font-bold text-ttg11-ink">Delete production database</b>
<span class="text-[12.5px] text-ttg11-mute">Hold the sensor for 1.2s to arm</span>
</div>
<button type="button" id="ttg-11-btn" role="switch" aria-checked="false" class="relative grid place-items-center w-26 h-26 rounded-full border-0 overflow-hidden cursor-pointer select-none [touch-action:none] bg-ttg11-sensor text-ttg11-idle transition-colors duration-300 aria-checked:text-ttg11-ok focus-visible:outline-3 focus-visible:outline-ttg11-scanline focus-visible:outline-offset-4 data-holding:[&_.ttg11-scan]:opacity-100 data-holding:[&_.ttg11-scan]:animate-ttg11-scan data-holding:[&_.ttg11-ring]:animate-ttg11-fill motion-reduce:data-holding:[&_.ttg11-scan]:animate-none">
<svg viewBox="0 0 40 48" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" class="relative z-10 w-[46px] h-14 transition-[filter] duration-300 [.ttg11-btn[aria-checked=true]_&]:drop-shadow-[0_0_8px_currentColor]">
<path d="M20 6c-8 0-13 6-13 13v6"/><path d="M20 12c-5 0-8 4-8 8v9"/><path d="M20 18c-2.5 0-4 2-4 4v12"/><path d="M20 25v14"/><path d="M25 21c.6 1 .9 2.2.9 3.5V37"/><path d="M29.5 17c1.6 2 2.5 4.5 2.5 7v9"/><path d="M27 9.5c3.7 2.3 6 6.3 6 10.5v3"/>
</svg>
<span aria-hidden="true" class="ttg11-scan absolute left-[18px] right-[18px] top-0 z-20 h-[3px] rounded-sm opacity-0 bg-ttg11-scanline shadow-[0_0_12px_var(--color-ttg11-scanline)]"></span>
<span aria-hidden="true" class="ttg11-ring absolute inset-0 rounded-full [background:conic-gradient(var(--color-ttg11-ok)_var(--ttg11-p,0deg),transparent_0)] [mask:radial-gradient(circle,transparent_46px,#000_47px)]"></span>
</button>
<p id="ttg-11-state" aria-live="polite" class="min-h-[1.2em] text-[13px] font-semibold tracking-[0.08em] uppercase text-ttg11-mute">Locked</p>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@property --ttg11-p {
syntax: '<angle>';
inherits: false;
initial-value: 0deg;
}
@theme {
--color-ttg11-bg: #0f1219;
--color-ttg11-bg: oklch(0.18 0.012 265);
--color-ttg11-card: #171c26;
--color-ttg11-card: oklch(0.23 0.014 265);
--color-ttg11-edge: #262d3c;
--color-ttg11-edge: oklch(0.3 0.016 265);
--color-ttg11-sensor: #1f2533;
--color-ttg11-sensor: oklch(0.26 0.018 265);
--color-ttg11-idle: #737d94;
--color-ttg11-idle: oklch(0.58 0.02 265);
--color-ttg11-ok: #4fd398;
--color-ttg11-ok: oklch(0.79 0.15 160);
--color-ttg11-scanline: #58b6f0;
--color-ttg11-scanline: oklch(0.75 0.14 230);
--color-ttg11-ink: #e7eaf3;
--color-ttg11-ink: oklch(0.93 0.008 265);
--color-ttg11-mute: #8d95a8;
--color-ttg11-mute: oklch(0.66 0.016 265);
--font-ttg11-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--animate-ttg11-scan: ttg11-scan 1.2s ease-in-out;
--animate-ttg11-fill: ttg11-fill 1.2s linear forwards;
}
@keyframes ttg11-scan {
0% {
transform: translateY(14px);
}
50% {
transform: translateY(88px);
}
100% {
transform: translateY(14px);
}
}
@keyframes ttg11-fill {
from {
--ttg11-p: 0deg;
}
to {
--ttg11-p: 360deg;
}
}
/* The armed ring has to stay full after the animation is removed, and an aria state
cannot set a custom property through a utility. */
.ttg-11 [aria-checked="true"] .ttg11-ring {
--ttg11-p: 360deg;
}
.ttg-11 {
width: 100%;
min-height: 100svh;
}
```
## JavaScript
```js
(() => {
const HOLD_MS = 1200;
const btn = document.querySelector('#ttg-11-btn');
const state = document.querySelector('#ttg-11-state');
if (!btn || !state) return;
let timer = null;
const arm = () => {
if (timer) return;
btn.setAttribute('data-holding', '');
state.textContent = 'Scanning\u2026';
timer = setTimeout(() => {
timer = null;
btn.removeAttribute('data-holding');
const next = btn.getAttribute('aria-checked') !== 'true';
btn.setAttribute('aria-checked', String(next));
state.textContent = next ? 'Armed \u2014 verified' : 'Locked';
}, HOLD_MS);
};
const disarm = () => {
if (!timer) return;
clearTimeout(timer);
timer = null;
btn.removeAttribute('data-holding');
state.textContent = 'Released too early';
};
btn.addEventListener('pointerdown', (e) => { btn.setPointerCapture(e.pointerId); arm(); });
['pointerup', 'pointercancel', 'pointerleave'].forEach((t) => btn.addEventListener(t, disarm));
btn.addEventListener('keydown', (e) => {
if ((e.key === ' ' || e.key === 'Enter') && !e.repeat) { e.preventDefault(); arm(); }
});
btn.addEventListener('keyup', (e) => { if (e.key === ' ' || e.key === 'Enter') disarm(); });
})();
```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 Press-and-Hold to Confirm Toggle (Fingerprint Scan)
Source: https://codefronts.com/components/tailwind-css-toggles-switches/tailwind-press-and-hold-to-confirm-toggle-fingerprint/
A destructive-action guard shaped like a fingerprint sensor: press and hold 1.2s while a scan line sweeps and a ring fills. Release early and it disarms. The state lives on aria-checked, so the styling variant and the accessibility contract are literally the same attribute.
## HTML
```html
<section class="ttg-11 w-full min-h-svh grid place-items-center px-5 py-14 bg-ttg11-bg font-ttg11-sans" aria-label="Hold to confirm toggle">
<div class="w-[min(340px,100%)] flex flex-col items-center gap-5 px-10 py-8 rounded-[22px] bg-ttg11-card border border-ttg11-edge">
<div class="flex flex-col items-center gap-1 text-center">
<b class="text-[16px] font-bold text-ttg11-ink">Delete production database</b>
<span class="text-[12.5px] text-ttg11-mute">Hold the sensor for 1.2s to arm</span>
</div>
<button type="button" id="ttg-11-btn" role="switch" aria-checked="false" class="relative grid place-items-center w-26 h-26 rounded-full border-0 overflow-hidden cursor-pointer select-none [touch-action:none] bg-ttg11-sensor text-ttg11-idle transition-colors duration-300 aria-checked:text-ttg11-ok focus-visible:outline-3 focus-visible:outline-ttg11-scanline focus-visible:outline-offset-4 data-holding:[&_.ttg11-scan]:opacity-100 data-holding:[&_.ttg11-scan]:animate-ttg11-scan data-holding:[&_.ttg11-ring]:animate-ttg11-fill motion-reduce:data-holding:[&_.ttg11-scan]:animate-none">
<svg viewBox="0 0 40 48" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" class="relative z-10 w-[46px] h-14 transition-[filter] duration-300 [.ttg11-btn[aria-checked=true]_&]:drop-shadow-[0_0_8px_currentColor]">
<path d="M20 6c-8 0-13 6-13 13v6"/><path d="M20 12c-5 0-8 4-8 8v9"/><path d="M20 18c-2.5 0-4 2-4 4v12"/><path d="M20 25v14"/><path d="M25 21c.6 1 .9 2.2.9 3.5V37"/><path d="M29.5 17c1.6 2 2.5 4.5 2.5 7v9"/><path d="M27 9.5c3.7 2.3 6 6.3 6 10.5v3"/>
</svg>
<span aria-hidden="true" class="ttg11-scan absolute left-[18px] right-[18px] top-0 z-20 h-[3px] rounded-sm opacity-0 bg-ttg11-scanline shadow-[0_0_12px_var(--color-ttg11-scanline)]"></span>
<span aria-hidden="true" class="ttg11-ring absolute inset-0 rounded-full [background:conic-gradient(var(--color-ttg11-ok)_var(--ttg11-p,0deg),transparent_0)] [mask:radial-gradient(circle,transparent_46px,#000_47px)]"></span>
</button>
<p id="ttg-11-state" aria-live="polite" class="min-h-[1.2em] text-[13px] font-semibold tracking-[0.08em] uppercase text-ttg11-mute">Locked</p>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@property --ttg11-p {
syntax: '<angle>';
inherits: false;
initial-value: 0deg;
}
@theme {
--color-ttg11-bg: #0f1219;
--color-ttg11-bg: oklch(0.18 0.012 265);
--color-ttg11-card: #171c26;
--color-ttg11-card: oklch(0.23 0.014 265);
--color-ttg11-edge: #262d3c;
--color-ttg11-edge: oklch(0.3 0.016 265);
--color-ttg11-sensor: #1f2533;
--color-ttg11-sensor: oklch(0.26 0.018 265);
--color-ttg11-idle: #737d94;
--color-ttg11-idle: oklch(0.58 0.02 265);
--color-ttg11-ok: #4fd398;
--color-ttg11-ok: oklch(0.79 0.15 160);
--color-ttg11-scanline: #58b6f0;
--color-ttg11-scanline: oklch(0.75 0.14 230);
--color-ttg11-ink: #e7eaf3;
--color-ttg11-ink: oklch(0.93 0.008 265);
--color-ttg11-mute: #8d95a8;
--color-ttg11-mute: oklch(0.66 0.016 265);
--font-ttg11-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--animate-ttg11-scan: ttg11-scan 1.2s ease-in-out;
--animate-ttg11-fill: ttg11-fill 1.2s linear forwards;
}
@keyframes ttg11-scan {
0% {
transform: translateY(14px);
}
50% {
transform: translateY(88px);
}
100% {
transform: translateY(14px);
}
}
@keyframes ttg11-fill {
from {
--ttg11-p: 0deg;
}
to {
--ttg11-p: 360deg;
}
}
/* The armed ring has to stay full after the animation is removed, and an aria state
cannot set a custom property through a utility. */
.ttg-11 [aria-checked="true"] .ttg11-ring {
--ttg11-p: 360deg;
}
.ttg-11 {
width: 100%;
min-height: 100svh;
}
```
## JavaScript
```js
(() => {
const HOLD_MS = 1200;
const btn = document.querySelector('#ttg-11-btn');
const state = document.querySelector('#ttg-11-state');
if (!btn || !state) return;
let timer = null;
const arm = () => {
if (timer) return;
btn.setAttribute('data-holding', '');
state.textContent = 'Scanning\u2026';
timer = setTimeout(() => {
timer = null;
btn.removeAttribute('data-holding');
const next = btn.getAttribute('aria-checked') !== 'true';
btn.setAttribute('aria-checked', String(next));
state.textContent = next ? 'Armed \u2014 verified' : 'Locked';
}, HOLD_MS);
};
const disarm = () => {
if (!timer) return;
clearTimeout(timer);
timer = null;
btn.removeAttribute('data-holding');
state.textContent = 'Released too early';
};
btn.addEventListener('pointerdown', (e) => { btn.setPointerCapture(e.pointerId); arm(); });
['pointerup', 'pointercancel', 'pointerleave'].forEach((t) => btn.addEventListener(t, disarm));
btn.addEventListener('keydown', (e) => {
if ((e.key === ' ' || e.key === 'Enter') && !e.repeat) { e.preventDefault(); arm(); }
});
btn.addEventListener('keyup', (e) => { if (e.key === ' ' || e.key === 'Enter') disarm(); });
})();
```How this works
A button, not a checkbox. A hold gesture has no native input, so this is <button role="switch" aria-checked>. That single attribute is both the accessible state and the styling hook: aria-checked: colours the print, lights the glow and pins the ring at full.
The timer decides; the animation only illustrates. Pointer-down sets data-holding and starts a 1.2s setTimeout. Only the timeout flips aria-checked. Pointer-up, -cancel or -leave clears both, so a nervous tap cannot half-arm the control.
Keyboard parity is not optional. Space and Enter start the same hold on keydown (ignoring auto-repeat) and end it on keyup. A confirm gesture that only works with a mouse is a confirm gesture that locks out keyboard users.
Make it yours
- Tune the hold in two matched places:
HOLD_MSand the--animate-ttg11-*durations. - Swap the fingerprint path for a shield or lock glyph for non-biometric confirms.
- Add
navigator.vibrate?.(12)on success for a physical unlock tick. - Invert it: require the hold to turn OFF, guarding disable instead of enable.
Gotchas — read before shipping
- Bind
pointerdown,pointerup,pointercancelANDpointerleave. Miss one and the button sticks armed after a drag-off. - Ignore
e.repeaton keydown, or holding Space fires dozens of starts per second. - This is a confirmation affordance, NOT authentication. Never label it in a way that implies real biometric security.
[touch-action:none]is load-bearing on mobile: without it the browser claims the gesture for scrolling before 1.2s elapses.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
Pointer Events and button[role=switch] are universal; @property smooths the ring fill (Firefox 128+) and degrades to a stepped ring.