10 / 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 Timed Auto-Off Toggle with Conic Countdown Ring
A switch that disarms itself: flip it on and a conic ring drains for 8 seconds, then the toggle flips back off with the change announced politely. The ring is a registered custom property the browser tweens natively — no requestAnimationFrame loop anywhere.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="ttg-10 w-full min-h-svh grid place-items-center px-5 py-14 bg-ttg10-bg font-ttg10-sans" aria-label="Timed auto-off toggle">
<div class="w-[min(340px,100%)] flex flex-col items-center gap-5 px-9 py-8 rounded-[22px] bg-ttg10-card border border-ttg10-edge">
<div class="flex flex-col items-center gap-1 text-center">
<b class="text-[17px] font-bold text-ttg10-ink">Temporarily allow</b>
<span class="text-[12.5px] text-ttg10-mute">USB debugging · auto-off in 8s</span>
</div>
<span id="ttg-10-ring" class="relative grid place-items-center w-28 h-28 rounded-full [background:conic-gradient(var(--color-ttg10-warn)_var(--ttg10-p,100%),var(--color-ttg10-rail)_0)] before:content-[''] before:absolute before:inset-[5px] before:rounded-full before:bg-ttg10-card data-counting:animate-ttg10-drain">
<input type="checkbox" id="ttg-10-sw" role="switch" aria-label="Temporarily allow USB debugging" class="appearance-none relative z-10 w-16 h-9 rounded-full bg-ttg10-off cursor-pointer transition-colors duration-300 before:content-[''] before:absolute before:top-[3px] before:left-[3px] before:w-[30px] before:h-[30px] before:rounded-full before:bg-white before:shadow-[0_2px_6px_oklch(0_0_0/0.5)] before:transition-transform before:duration-300 before:ease-[cubic-bezier(0.25,0.9,0.3,1.1)] checked:bg-ttg10-warn checked:before:translate-x-7 focus-visible:outline-3 focus-visible:outline-ttg10-warn focus-visible:outline-offset-4 motion-reduce:transition-none motion-reduce:before:transition-none">
</span>
<p id="ttg-10-live" aria-live="polite" class="min-h-[1.2em] text-center text-[12.5px] text-ttg10-mute">Off — flip to allow for 8 seconds</p>
</div>
</section><section class="ttg-10 w-full min-h-svh grid place-items-center px-5 py-14 bg-ttg10-bg font-ttg10-sans" aria-label="Timed auto-off toggle">
<div class="w-[min(340px,100%)] flex flex-col items-center gap-5 px-9 py-8 rounded-[22px] bg-ttg10-card border border-ttg10-edge">
<div class="flex flex-col items-center gap-1 text-center">
<b class="text-[17px] font-bold text-ttg10-ink">Temporarily allow</b>
<span class="text-[12.5px] text-ttg10-mute">USB debugging · auto-off in 8s</span>
</div>
<span id="ttg-10-ring" class="relative grid place-items-center w-28 h-28 rounded-full [background:conic-gradient(var(--color-ttg10-warn)_var(--ttg10-p,100%),var(--color-ttg10-rail)_0)] before:content-[''] before:absolute before:inset-[5px] before:rounded-full before:bg-ttg10-card data-counting:animate-ttg10-drain">
<input type="checkbox" id="ttg-10-sw" role="switch" aria-label="Temporarily allow USB debugging" class="appearance-none relative z-10 w-16 h-9 rounded-full bg-ttg10-off cursor-pointer transition-colors duration-300 before:content-[''] before:absolute before:top-[3px] before:left-[3px] before:w-[30px] before:h-[30px] before:rounded-full before:bg-white before:shadow-[0_2px_6px_oklch(0_0_0/0.5)] before:transition-transform before:duration-300 before:ease-[cubic-bezier(0.25,0.9,0.3,1.1)] checked:bg-ttg10-warn checked:before:translate-x-7 focus-visible:outline-3 focus-visible:outline-ttg10-warn focus-visible:outline-offset-4 motion-reduce:transition-none motion-reduce:before:transition-none">
</span>
<p id="ttg-10-live" aria-live="polite" class="min-h-[1.2em] text-center text-[12.5px] text-ttg10-mute">Off — flip to allow for 8 seconds</p>
</div>
</section>@import "tailwindcss";
/* Registration is what makes the sweep interpolable; without it the conic gradient
jumps between computed values instead of animating. */
@property --ttg10-p {
syntax: '<percentage>';
inherits: false;
initial-value: 100%;
}
@theme {
--color-ttg10-bg: #14161c;
--color-ttg10-bg: oklch(0.2 0.01 265);
--color-ttg10-card: #1c2029;
--color-ttg10-card: oklch(0.25 0.012 265);
--color-ttg10-edge: #2b303c;
--color-ttg10-edge: oklch(0.32 0.014 265);
--color-ttg10-rail: #2b303c;
--color-ttg10-rail: oklch(0.32 0.014 265);
--color-ttg10-off: #343a4a;
--color-ttg10-off: oklch(0.36 0.018 265);
--color-ttg10-warn: #eea23c;
--color-ttg10-warn: oklch(0.78 0.15 68);
--color-ttg10-ink: #e8eaf2;
--color-ttg10-ink: oklch(0.93 0.008 265);
--color-ttg10-mute: #939bad;
--color-ttg10-mute: oklch(0.68 0.015 265);
--font-ttg10-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--animate-ttg10-drain: ttg10-drain 8s linear forwards;
}
@keyframes ttg10-drain {
from {
--ttg10-p: 100%;
}
to {
--ttg10-p: 0%;
}
}
.ttg-10 {
width: 100%;
min-height: 100svh;
}
.ttg-10 input[type="checkbox"] {
-webkit-appearance: none;
}@import "tailwindcss";
/* Registration is what makes the sweep interpolable; without it the conic gradient
jumps between computed values instead of animating. */
@property --ttg10-p {
syntax: '<percentage>';
inherits: false;
initial-value: 100%;
}
@theme {
--color-ttg10-bg: #14161c;
--color-ttg10-bg: oklch(0.2 0.01 265);
--color-ttg10-card: #1c2029;
--color-ttg10-card: oklch(0.25 0.012 265);
--color-ttg10-edge: #2b303c;
--color-ttg10-edge: oklch(0.32 0.014 265);
--color-ttg10-rail: #2b303c;
--color-ttg10-rail: oklch(0.32 0.014 265);
--color-ttg10-off: #343a4a;
--color-ttg10-off: oklch(0.36 0.018 265);
--color-ttg10-warn: #eea23c;
--color-ttg10-warn: oklch(0.78 0.15 68);
--color-ttg10-ink: #e8eaf2;
--color-ttg10-ink: oklch(0.93 0.008 265);
--color-ttg10-mute: #939bad;
--color-ttg10-mute: oklch(0.68 0.015 265);
--font-ttg10-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--animate-ttg10-drain: ttg10-drain 8s linear forwards;
}
@keyframes ttg10-drain {
from {
--ttg10-p: 100%;
}
to {
--ttg10-p: 0%;
}
}
.ttg-10 {
width: 100%;
min-height: 100svh;
}
.ttg-10 input[type="checkbox"] {
-webkit-appearance: none;
}(() => {
const DURATION = 8000;
const sw = document.querySelector('#ttg-10-sw');
const ring = document.querySelector('#ttg-10-ring');
const live = document.querySelector('#ttg-10-live');
if (!sw || !ring || !live) return;
let timer = null;
// The script owns exactly one attribute; every visual consequence is a Tailwind variant.
const count = (on) => {
ring.removeAttribute('data-counting');
if (!on) return;
void ring.offsetWidth; // restart the CSS countdown
ring.setAttribute('data-counting', '');
};
sw.addEventListener('change', () => {
clearTimeout(timer);
count(sw.checked);
if (!sw.checked) { live.textContent = 'Disabled'; return; }
live.textContent = 'Enabled for 8 seconds';
timer = setTimeout(() => {
sw.checked = false;
count(false);
live.textContent = 'Auto-disabled';
}, DURATION);
});
})();(() => {
const DURATION = 8000;
const sw = document.querySelector('#ttg-10-sw');
const ring = document.querySelector('#ttg-10-ring');
const live = document.querySelector('#ttg-10-live');
if (!sw || !ring || !live) return;
let timer = null;
// The script owns exactly one attribute; every visual consequence is a Tailwind variant.
const count = (on) => {
ring.removeAttribute('data-counting');
if (!on) return;
void ring.offsetWidth; // restart the CSS countdown
ring.setAttribute('data-counting', '');
};
sw.addEventListener('change', () => {
clearTimeout(timer);
count(sw.checked);
if (!sw.checked) { live.textContent = 'Disabled'; return; }
live.textContent = 'Enabled for 8 seconds';
timer = setTimeout(() => {
sw.checked = false;
count(false);
live.textContent = 'Auto-disabled';
}, DURATION);
});
})();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";
/* Registration is what makes the sweep interpolable; without it the conic gradient
jumps between computed values instead of animating. */
@property --ttg10-p {
syntax: '<percentage>';
inherits: false;
initial-value: 100%;
}
@theme {
--color-ttg10-bg: #14161c;
--color-ttg10-bg: oklch(0.2 0.01 265);
--color-ttg10-card: #1c2029;
--color-ttg10-card: oklch(0.25 0.012 265);
--color-ttg10-edge: #2b303c;
--color-ttg10-edge: oklch(0.32 0.014 265);
--color-ttg10-rail: #2b303c;
--color-ttg10-rail: oklch(0.32 0.014 265);
--color-ttg10-off: #343a4a;
--color-ttg10-off: oklch(0.36 0.018 265);
--color-ttg10-warn: #eea23c;
--color-ttg10-warn: oklch(0.78 0.15 68);
--color-ttg10-ink: #e8eaf2;
--color-ttg10-ink: oklch(0.93 0.008 265);
--color-ttg10-mute: #939bad;
--color-ttg10-mute: oklch(0.68 0.015 265);
--font-ttg10-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--animate-ttg10-drain: ttg10-drain 8s linear forwards;
}
@keyframes ttg10-drain {
from {
--ttg10-p: 100%;
}
to {
--ttg10-p: 0%;
}
}
.ttg-10 {
width: 100%;
min-height: 100svh;
}
.ttg-10 input[type="checkbox"] {
-webkit-appearance: none;
}@import "tailwindcss";
/* Registration is what makes the sweep interpolable; without it the conic gradient
jumps between computed values instead of animating. */
@property --ttg10-p {
syntax: '<percentage>';
inherits: false;
initial-value: 100%;
}
@theme {
--color-ttg10-bg: #14161c;
--color-ttg10-bg: oklch(0.2 0.01 265);
--color-ttg10-card: #1c2029;
--color-ttg10-card: oklch(0.25 0.012 265);
--color-ttg10-edge: #2b303c;
--color-ttg10-edge: oklch(0.32 0.014 265);
--color-ttg10-rail: #2b303c;
--color-ttg10-rail: oklch(0.32 0.014 265);
--color-ttg10-off: #343a4a;
--color-ttg10-off: oklch(0.36 0.018 265);
--color-ttg10-warn: #eea23c;
--color-ttg10-warn: oklch(0.78 0.15 68);
--color-ttg10-ink: #e8eaf2;
--color-ttg10-ink: oklch(0.93 0.008 265);
--color-ttg10-mute: #939bad;
--color-ttg10-mute: oklch(0.68 0.015 265);
--font-ttg10-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--animate-ttg10-drain: ttg10-drain 8s linear forwards;
}
@keyframes ttg10-drain {
from {
--ttg10-p: 100%;
}
to {
--ttg10-p: 0%;
}
}
.ttg-10 {
width: 100%;
min-height: 100svh;
}
.ttg-10 input[type="checkbox"] {
-webkit-appearance: none;
}<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.
data-counting:is the port's structural win: the CSS version toggled a.tgl-10--countingclass, which meant the script knew a style name. Now the script sets a semantic attribute and Tailwind decides what it looks like.@propertyand@keyframesboth stay in plain CSS — v4 mints theanimate-*utility from the--animate-*token, but the block it refers to is still ordinary CSS, and property registration has no utility form at all.- The conic gradient is an arbitrary
[background:...]rather than a theme entry, because the sweep stop is a live variable, not a design decision. - UI change from the CSS original: the live region now carries a resting message ("Off — flip to allow for 8 seconds") instead of being empty, so the control explains its own behaviour before you touch it, and the warn amber moved to
oklch(0.78 0.15 68)for 8:1 against the card.
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 Timed Auto-Off Toggle with Conic Countdown Ring
Source: https://codefronts.com/components/tailwind-css-toggles-switches/tailwind-timed-auto-off-toggle-conic-countdown-ring/
A switch that disarms itself: flip it on and a conic ring drains for 8 seconds, then the toggle flips back off with the change announced politely. The ring is a registered custom property the browser tweens natively — no requestAnimationFrame loop anywhere.
## HTML
```html
<section class="ttg-10 w-full min-h-svh grid place-items-center px-5 py-14 bg-ttg10-bg font-ttg10-sans" aria-label="Timed auto-off toggle">
<div class="w-[min(340px,100%)] flex flex-col items-center gap-5 px-9 py-8 rounded-[22px] bg-ttg10-card border border-ttg10-edge">
<div class="flex flex-col items-center gap-1 text-center">
<b class="text-[17px] font-bold text-ttg10-ink">Temporarily allow</b>
<span class="text-[12.5px] text-ttg10-mute">USB debugging · auto-off in 8s</span>
</div>
<span id="ttg-10-ring" class="relative grid place-items-center w-28 h-28 rounded-full [background:conic-gradient(var(--color-ttg10-warn)_var(--ttg10-p,100%),var(--color-ttg10-rail)_0)] before:content-[''] before:absolute before:inset-[5px] before:rounded-full before:bg-ttg10-card data-counting:animate-ttg10-drain">
<input type="checkbox" id="ttg-10-sw" role="switch" aria-label="Temporarily allow USB debugging" class="appearance-none relative z-10 w-16 h-9 rounded-full bg-ttg10-off cursor-pointer transition-colors duration-300 before:content-[''] before:absolute before:top-[3px] before:left-[3px] before:w-[30px] before:h-[30px] before:rounded-full before:bg-white before:shadow-[0_2px_6px_oklch(0_0_0/0.5)] before:transition-transform before:duration-300 before:ease-[cubic-bezier(0.25,0.9,0.3,1.1)] checked:bg-ttg10-warn checked:before:translate-x-7 focus-visible:outline-3 focus-visible:outline-ttg10-warn focus-visible:outline-offset-4 motion-reduce:transition-none motion-reduce:before:transition-none">
</span>
<p id="ttg-10-live" aria-live="polite" class="min-h-[1.2em] text-center text-[12.5px] text-ttg10-mute">Off — flip to allow for 8 seconds</p>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
/* Registration is what makes the sweep interpolable; without it the conic gradient
jumps between computed values instead of animating. */
@property --ttg10-p {
syntax: '<percentage>';
inherits: false;
initial-value: 100%;
}
@theme {
--color-ttg10-bg: #14161c;
--color-ttg10-bg: oklch(0.2 0.01 265);
--color-ttg10-card: #1c2029;
--color-ttg10-card: oklch(0.25 0.012 265);
--color-ttg10-edge: #2b303c;
--color-ttg10-edge: oklch(0.32 0.014 265);
--color-ttg10-rail: #2b303c;
--color-ttg10-rail: oklch(0.32 0.014 265);
--color-ttg10-off: #343a4a;
--color-ttg10-off: oklch(0.36 0.018 265);
--color-ttg10-warn: #eea23c;
--color-ttg10-warn: oklch(0.78 0.15 68);
--color-ttg10-ink: #e8eaf2;
--color-ttg10-ink: oklch(0.93 0.008 265);
--color-ttg10-mute: #939bad;
--color-ttg10-mute: oklch(0.68 0.015 265);
--font-ttg10-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--animate-ttg10-drain: ttg10-drain 8s linear forwards;
}
@keyframes ttg10-drain {
from {
--ttg10-p: 100%;
}
to {
--ttg10-p: 0%;
}
}
.ttg-10 {
width: 100%;
min-height: 100svh;
}
.ttg-10 input[type="checkbox"] {
-webkit-appearance: none;
}
```
## JavaScript
```js
(() => {
const DURATION = 8000;
const sw = document.querySelector('#ttg-10-sw');
const ring = document.querySelector('#ttg-10-ring');
const live = document.querySelector('#ttg-10-live');
if (!sw || !ring || !live) return;
let timer = null;
// The script owns exactly one attribute; every visual consequence is a Tailwind variant.
const count = (on) => {
ring.removeAttribute('data-counting');
if (!on) return;
void ring.offsetWidth; // restart the CSS countdown
ring.setAttribute('data-counting', '');
};
sw.addEventListener('change', () => {
clearTimeout(timer);
count(sw.checked);
if (!sw.checked) { live.textContent = 'Disabled'; return; }
live.textContent = 'Enabled for 8 seconds';
timer = setTimeout(() => {
sw.checked = false;
count(false);
live.textContent = 'Auto-disabled';
}, DURATION);
});
})();
```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 Timed Auto-Off Toggle with Conic Countdown Ring
Source: https://codefronts.com/components/tailwind-css-toggles-switches/tailwind-timed-auto-off-toggle-conic-countdown-ring/
A switch that disarms itself: flip it on and a conic ring drains for 8 seconds, then the toggle flips back off with the change announced politely. The ring is a registered custom property the browser tweens natively — no requestAnimationFrame loop anywhere.
## HTML
```html
<section class="ttg-10 w-full min-h-svh grid place-items-center px-5 py-14 bg-ttg10-bg font-ttg10-sans" aria-label="Timed auto-off toggle">
<div class="w-[min(340px,100%)] flex flex-col items-center gap-5 px-9 py-8 rounded-[22px] bg-ttg10-card border border-ttg10-edge">
<div class="flex flex-col items-center gap-1 text-center">
<b class="text-[17px] font-bold text-ttg10-ink">Temporarily allow</b>
<span class="text-[12.5px] text-ttg10-mute">USB debugging · auto-off in 8s</span>
</div>
<span id="ttg-10-ring" class="relative grid place-items-center w-28 h-28 rounded-full [background:conic-gradient(var(--color-ttg10-warn)_var(--ttg10-p,100%),var(--color-ttg10-rail)_0)] before:content-[''] before:absolute before:inset-[5px] before:rounded-full before:bg-ttg10-card data-counting:animate-ttg10-drain">
<input type="checkbox" id="ttg-10-sw" role="switch" aria-label="Temporarily allow USB debugging" class="appearance-none relative z-10 w-16 h-9 rounded-full bg-ttg10-off cursor-pointer transition-colors duration-300 before:content-[''] before:absolute before:top-[3px] before:left-[3px] before:w-[30px] before:h-[30px] before:rounded-full before:bg-white before:shadow-[0_2px_6px_oklch(0_0_0/0.5)] before:transition-transform before:duration-300 before:ease-[cubic-bezier(0.25,0.9,0.3,1.1)] checked:bg-ttg10-warn checked:before:translate-x-7 focus-visible:outline-3 focus-visible:outline-ttg10-warn focus-visible:outline-offset-4 motion-reduce:transition-none motion-reduce:before:transition-none">
</span>
<p id="ttg-10-live" aria-live="polite" class="min-h-[1.2em] text-center text-[12.5px] text-ttg10-mute">Off — flip to allow for 8 seconds</p>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
/* Registration is what makes the sweep interpolable; without it the conic gradient
jumps between computed values instead of animating. */
@property --ttg10-p {
syntax: '<percentage>';
inherits: false;
initial-value: 100%;
}
@theme {
--color-ttg10-bg: #14161c;
--color-ttg10-bg: oklch(0.2 0.01 265);
--color-ttg10-card: #1c2029;
--color-ttg10-card: oklch(0.25 0.012 265);
--color-ttg10-edge: #2b303c;
--color-ttg10-edge: oklch(0.32 0.014 265);
--color-ttg10-rail: #2b303c;
--color-ttg10-rail: oklch(0.32 0.014 265);
--color-ttg10-off: #343a4a;
--color-ttg10-off: oklch(0.36 0.018 265);
--color-ttg10-warn: #eea23c;
--color-ttg10-warn: oklch(0.78 0.15 68);
--color-ttg10-ink: #e8eaf2;
--color-ttg10-ink: oklch(0.93 0.008 265);
--color-ttg10-mute: #939bad;
--color-ttg10-mute: oklch(0.68 0.015 265);
--font-ttg10-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--animate-ttg10-drain: ttg10-drain 8s linear forwards;
}
@keyframes ttg10-drain {
from {
--ttg10-p: 100%;
}
to {
--ttg10-p: 0%;
}
}
.ttg-10 {
width: 100%;
min-height: 100svh;
}
.ttg-10 input[type="checkbox"] {
-webkit-appearance: none;
}
```
## JavaScript
```js
(() => {
const DURATION = 8000;
const sw = document.querySelector('#ttg-10-sw');
const ring = document.querySelector('#ttg-10-ring');
const live = document.querySelector('#ttg-10-live');
if (!sw || !ring || !live) return;
let timer = null;
// The script owns exactly one attribute; every visual consequence is a Tailwind variant.
const count = (on) => {
ring.removeAttribute('data-counting');
if (!on) return;
void ring.offsetWidth; // restart the CSS countdown
ring.setAttribute('data-counting', '');
};
sw.addEventListener('change', () => {
clearTimeout(timer);
count(sw.checked);
if (!sw.checked) { live.textContent = 'Disabled'; return; }
live.textContent = 'Enabled for 8 seconds';
timer = setTimeout(() => {
sw.checked = false;
count(false);
live.textContent = 'Auto-disabled';
}, DURATION);
});
})();
```How this works
The ring is CSS, the clock is JS, and they never disagree. @property --ttg10-p { syntax: '<percentage>' } makes the sweep stop animatable; an 8s linear keyframe drives it 100% → 0%. JavaScript starts the same 8s setTimeout and is the single source of truth for state — the animation is decoration that happens to be exactly as long.
One attribute, many utilities. The script toggles data-counting on the ring and nothing else. data-counting:animate-ttg10-drain does the rest, which is the whole argument for data-attribute variants: the JavaScript stays a state machine and never touches a class list or a style property.
Restarting an animation still needs the reflow trick. Removing the attribute, reading offsetWidth and re-adding it is the one imperative wart, and it is unavoidable in CSS-driven countdowns.
Make it yours
- Change the window in one place:
DURATIONin the script and the duration in the--animate-ttg10-draintoken. - Warn near expiry: add a second keyframe that flashes the rail red below 25%.
- Persist across reloads by mirroring the deadline into
localStorageand re-arming on load. - Retheme through
--color-ttg10-warn; ring, track and focus ring all read it.
Gotchas — read before shipping
- Without
@propertythe conic gradient snaps between frames instead of sweeping. The fallback is a static ring, which is why the timer must not depend on the animation. - Never derive real state from an animation's end —
animationenddoes not fire if the tab is backgrounded at the wrong moment; the timeout does. - An
aria-liveregion must be in the DOM before you write to it. Injecting the region and its text together loses the first announcement in most screen readers.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
@property drives the smooth sweep (Firefox 128+); on older engines the ring is static and the auto-off behaviour is unaffected.