09 / 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 Waveform Mute Toggle with Animated EQ Bars
A mute switch that shows sound instead of describing it: five bars dance while audio is live and snap to a flatline the instant you mute. --animate-ttg09-eq in @theme mints the animate-ttg09-eq utility; per-bar delays stay inline, because a stagger is data, not a design token.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="ttg-09 w-full min-h-svh flex flex-col items-center justify-center gap-7 px-5 py-14 bg-ttg09-bg font-ttg09-sans" aria-label="Waveform mute toggle">
<label for="ttg-09-sw" class="flex items-center gap-8 px-7 py-5 rounded-3xl cursor-pointer bg-ttg09-card border border-ttg09-edge">
<span class="flex flex-col gap-0.5">
<span class="text-[15.5px] font-semibold text-ttg09-ink">Studio monitor</span>
<span class="text-[12.5px] text-ttg09-mute">Routes to the desk, not the interface</span>
</span>
<span class="relative shrink-0 w-24 h-12 rounded-[26px] bg-ttg09-track transition-shadow duration-300 shadow-[inset_0_2px_6px_oklch(0_0_0/0.6),inset_0_0_0_1px_var(--color-ttg09-edge)] has-checked:shadow-[inset_0_2px_6px_oklch(0_0_0/0.6),inset_0_0_0_1px_color-mix(in_oklab,var(--color-ttg09-live)_55%,transparent),0_0_18px_-4px_color-mix(in_oklab,var(--color-ttg09-live)_60%,transparent)]">
<input type="checkbox" id="ttg-09-sw" role="switch" checked class="peer appearance-none absolute inset-0 w-full h-full rounded-[26px] bg-transparent cursor-pointer z-10 focus-visible:outline-3 focus-visible:outline-ttg09-live focus-visible:outline-offset-4">
<span aria-hidden="true" class="absolute inset-0 flex items-center justify-center gap-[5px] pointer-events-none peer-checked:[&_i]:bg-ttg09-live peer-checked:[&_i]:[animation:ttg09-eq_var(--dur,0.9s)_ease-in-out_var(--d)_infinite_alternate] motion-reduce:peer-checked:[&_i]:[animation:none] motion-reduce:peer-checked:[&_i]:scale-y-[0.7] motion-reduce:peer-checked:[&_i:nth-child(odd)]:scale-y-[0.45]">
<i style="--d:0s;" class="w-[5px] h-7 rounded-[3px] bg-ttg09-flat origin-center scale-y-[0.12] transition-[background-color,transform] duration-300"></i>
<i style="--d:.14s; --dur:0.75s;" class="w-[5px] h-7 rounded-[3px] bg-ttg09-flat origin-center scale-y-[0.12] transition-[background-color,transform] duration-300"></i>
<i style="--d:.28s;" class="w-[5px] h-7 rounded-[3px] bg-ttg09-flat origin-center scale-y-[0.12] transition-[background-color,transform] duration-300"></i>
<i style="--d:.07s; --dur:1.05s;" class="w-[5px] h-7 rounded-[3px] bg-ttg09-flat origin-center scale-y-[0.12] transition-[background-color,transform] duration-300"></i>
<i style="--d:.21s; --dur:0.8s;" class="w-[5px] h-7 rounded-[3px] bg-ttg09-flat origin-center scale-y-[0.12] transition-[background-color,transform] duration-300"></i>
</span>
</span>
</label>
<p class="font-ttg09-mono text-[11px] tracking-[0.14em] uppercase text-ttg09-mute">Live = dancing bars · muted = flatline</p>
</section><section class="ttg-09 w-full min-h-svh flex flex-col items-center justify-center gap-7 px-5 py-14 bg-ttg09-bg font-ttg09-sans" aria-label="Waveform mute toggle">
<label for="ttg-09-sw" class="flex items-center gap-8 px-7 py-5 rounded-3xl cursor-pointer bg-ttg09-card border border-ttg09-edge">
<span class="flex flex-col gap-0.5">
<span class="text-[15.5px] font-semibold text-ttg09-ink">Studio monitor</span>
<span class="text-[12.5px] text-ttg09-mute">Routes to the desk, not the interface</span>
</span>
<span class="relative shrink-0 w-24 h-12 rounded-[26px] bg-ttg09-track transition-shadow duration-300 shadow-[inset_0_2px_6px_oklch(0_0_0/0.6),inset_0_0_0_1px_var(--color-ttg09-edge)] has-checked:shadow-[inset_0_2px_6px_oklch(0_0_0/0.6),inset_0_0_0_1px_color-mix(in_oklab,var(--color-ttg09-live)_55%,transparent),0_0_18px_-4px_color-mix(in_oklab,var(--color-ttg09-live)_60%,transparent)]">
<input type="checkbox" id="ttg-09-sw" role="switch" checked class="peer appearance-none absolute inset-0 w-full h-full rounded-[26px] bg-transparent cursor-pointer z-10 focus-visible:outline-3 focus-visible:outline-ttg09-live focus-visible:outline-offset-4">
<span aria-hidden="true" class="absolute inset-0 flex items-center justify-center gap-[5px] pointer-events-none peer-checked:[&_i]:bg-ttg09-live peer-checked:[&_i]:[animation:ttg09-eq_var(--dur,0.9s)_ease-in-out_var(--d)_infinite_alternate] motion-reduce:peer-checked:[&_i]:[animation:none] motion-reduce:peer-checked:[&_i]:scale-y-[0.7] motion-reduce:peer-checked:[&_i:nth-child(odd)]:scale-y-[0.45]">
<i style="--d:0s;" class="w-[5px] h-7 rounded-[3px] bg-ttg09-flat origin-center scale-y-[0.12] transition-[background-color,transform] duration-300"></i>
<i style="--d:.14s; --dur:0.75s;" class="w-[5px] h-7 rounded-[3px] bg-ttg09-flat origin-center scale-y-[0.12] transition-[background-color,transform] duration-300"></i>
<i style="--d:.28s;" class="w-[5px] h-7 rounded-[3px] bg-ttg09-flat origin-center scale-y-[0.12] transition-[background-color,transform] duration-300"></i>
<i style="--d:.07s; --dur:1.05s;" class="w-[5px] h-7 rounded-[3px] bg-ttg09-flat origin-center scale-y-[0.12] transition-[background-color,transform] duration-300"></i>
<i style="--d:.21s; --dur:0.8s;" class="w-[5px] h-7 rounded-[3px] bg-ttg09-flat origin-center scale-y-[0.12] transition-[background-color,transform] duration-300"></i>
</span>
</span>
</label>
<p class="font-ttg09-mono text-[11px] tracking-[0.14em] uppercase text-ttg09-mute">Live = dancing bars · muted = flatline</p>
</section>@import "tailwindcss";
@theme {
--color-ttg09-bg: #0f1216;
--color-ttg09-bg: oklch(0.18 0.008 255);
--color-ttg09-card: #171b21;
--color-ttg09-card: oklch(0.23 0.01 255);
--color-ttg09-edge: #272d36;
--color-ttg09-edge: oklch(0.31 0.012 255);
--color-ttg09-track: #1b2028;
--color-ttg09-track: oklch(0.25 0.012 255);
--color-ttg09-flat: #454e5c;
--color-ttg09-flat: oklch(0.44 0.015 255);
--color-ttg09-live: #3ddc9a;
--color-ttg09-live: oklch(0.82 0.16 165);
--color-ttg09-ink: #d3dae4;
--color-ttg09-ink: oklch(0.89 0.01 255);
--color-ttg09-mute: #8d95a3;
--color-ttg09-mute: oklch(0.66 0.015 255);
--font-ttg09-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--font-ttg09-mono: 'JetBrains Mono', ui-monospace, Menlo, monospace;
--animate-ttg09-eq: ttg09-eq 0.9s ease-in-out infinite alternate;
}
/* The keyframes an --animate-* token refers to still live in plain CSS; v4 mints the
utility from the token, not the block. */
@keyframes ttg09-eq {
0% {
transform: scaleY(0.25);
}
100% {
transform: scaleY(1);
}
}
.ttg-09 {
width: 100%;
min-height: 100svh;
}
.ttg-09 input[type="checkbox"] {
-webkit-appearance: none;
}@import "tailwindcss";
@theme {
--color-ttg09-bg: #0f1216;
--color-ttg09-bg: oklch(0.18 0.008 255);
--color-ttg09-card: #171b21;
--color-ttg09-card: oklch(0.23 0.01 255);
--color-ttg09-edge: #272d36;
--color-ttg09-edge: oklch(0.31 0.012 255);
--color-ttg09-track: #1b2028;
--color-ttg09-track: oklch(0.25 0.012 255);
--color-ttg09-flat: #454e5c;
--color-ttg09-flat: oklch(0.44 0.015 255);
--color-ttg09-live: #3ddc9a;
--color-ttg09-live: oklch(0.82 0.16 165);
--color-ttg09-ink: #d3dae4;
--color-ttg09-ink: oklch(0.89 0.01 255);
--color-ttg09-mute: #8d95a3;
--color-ttg09-mute: oklch(0.66 0.015 255);
--font-ttg09-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--font-ttg09-mono: 'JetBrains Mono', ui-monospace, Menlo, monospace;
--animate-ttg09-eq: ttg09-eq 0.9s ease-in-out infinite alternate;
}
/* The keyframes an --animate-* token refers to still live in plain CSS; v4 mints the
utility from the token, not the block. */
@keyframes ttg09-eq {
0% {
transform: scaleY(0.25);
}
100% {
transform: scaleY(1);
}
}
.ttg-09 {
width: 100%;
min-height: 100svh;
}
.ttg-09 input[type="checkbox"] {
-webkit-appearance: none;
}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-ttg09-bg: #0f1216;
--color-ttg09-bg: oklch(0.18 0.008 255);
--color-ttg09-card: #171b21;
--color-ttg09-card: oklch(0.23 0.01 255);
--color-ttg09-edge: #272d36;
--color-ttg09-edge: oklch(0.31 0.012 255);
--color-ttg09-track: #1b2028;
--color-ttg09-track: oklch(0.25 0.012 255);
--color-ttg09-flat: #454e5c;
--color-ttg09-flat: oklch(0.44 0.015 255);
--color-ttg09-live: #3ddc9a;
--color-ttg09-live: oklch(0.82 0.16 165);
--color-ttg09-ink: #d3dae4;
--color-ttg09-ink: oklch(0.89 0.01 255);
--color-ttg09-mute: #8d95a3;
--color-ttg09-mute: oklch(0.66 0.015 255);
--font-ttg09-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--font-ttg09-mono: 'JetBrains Mono', ui-monospace, Menlo, monospace;
--animate-ttg09-eq: ttg09-eq 0.9s ease-in-out infinite alternate;
}
/* The keyframes an --animate-* token refers to still live in plain CSS; v4 mints the
utility from the token, not the block. */
@keyframes ttg09-eq {
0% {
transform: scaleY(0.25);
}
100% {
transform: scaleY(1);
}
}
.ttg-09 {
width: 100%;
min-height: 100svh;
}
.ttg-09 input[type="checkbox"] {
-webkit-appearance: none;
}@import "tailwindcss";
@theme {
--color-ttg09-bg: #0f1216;
--color-ttg09-bg: oklch(0.18 0.008 255);
--color-ttg09-card: #171b21;
--color-ttg09-card: oklch(0.23 0.01 255);
--color-ttg09-edge: #272d36;
--color-ttg09-edge: oklch(0.31 0.012 255);
--color-ttg09-track: #1b2028;
--color-ttg09-track: oklch(0.25 0.012 255);
--color-ttg09-flat: #454e5c;
--color-ttg09-flat: oklch(0.44 0.015 255);
--color-ttg09-live: #3ddc9a;
--color-ttg09-live: oklch(0.82 0.16 165);
--color-ttg09-ink: #d3dae4;
--color-ttg09-ink: oklch(0.89 0.01 255);
--color-ttg09-mute: #8d95a3;
--color-ttg09-mute: oklch(0.66 0.015 255);
--font-ttg09-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--font-ttg09-mono: 'JetBrains Mono', ui-monospace, Menlo, monospace;
--animate-ttg09-eq: ttg09-eq 0.9s ease-in-out infinite alternate;
}
/* The keyframes an --animate-* token refers to still live in plain CSS; v4 mints the
utility from the token, not the block. */
@keyframes ttg09-eq {
0% {
transform: scaleY(0.25);
}
100% {
transform: scaleY(1);
}
}
.ttg-09 {
width: 100%;
min-height: 100svh;
}
.ttg-09 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.
--animate-ttg09-eqplus a hand-written@keyframesblock is the complete v4 animation recipe: the token mintsanimate-ttg09-eq, the keyframes stay ordinary CSS. Trying to express the keyframes as utilities is the one thing this port does not attempt.- The stagger MUST ride inside the animation shorthand:
[animation:ttg09-eq_var(--dur,0.9s)_ease-in-out_var(--d)_infinite_alternate]. Tailwind'sanimate-*utility emits theanimationshorthand, which resetsanimation-delayandanimation-duration— and because it arrives here from a variant-scoped rule (.peer:checked ~ * i), it outranks any plain[animation-delay:var(--d)]on the bar itself. Written the obvious way, all five bars pulse in unison and the demo silently loses its point. - The reduced-motion fallback needed real thought:
animate-nonealone would flatline the bars and delete the live-state signal, somotion-reduce:also pins a static two-height pattern. - UI change from the CSS original: the switch gained a labelled card (it was a bare pill with a floating label), the live green moved to
oklch(0.82 0.16 165)for a 9:1 ratio against the track, and the muted bars lifted tooklch(0.44)so a flatline is still visibly a control rather than an empty pill.
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 Waveform Mute Toggle with Animated EQ Bars
Source: https://codefronts.com/components/tailwind-css-toggles-switches/tailwind-waveform-mute-toggle-animated-eq-bars/
A mute switch that shows sound instead of describing it: five bars dance while audio is live and snap to a flatline the instant you mute. --animate-ttg09-eq in @theme mints the animate-ttg09-eq utility; per-bar delays stay inline, because a stagger is data, not a design token.
## HTML
```html
<section class="ttg-09 w-full min-h-svh flex flex-col items-center justify-center gap-7 px-5 py-14 bg-ttg09-bg font-ttg09-sans" aria-label="Waveform mute toggle">
<label for="ttg-09-sw" class="flex items-center gap-8 px-7 py-5 rounded-3xl cursor-pointer bg-ttg09-card border border-ttg09-edge">
<span class="flex flex-col gap-0.5">
<span class="text-[15.5px] font-semibold text-ttg09-ink">Studio monitor</span>
<span class="text-[12.5px] text-ttg09-mute">Routes to the desk, not the interface</span>
</span>
<span class="relative shrink-0 w-24 h-12 rounded-[26px] bg-ttg09-track transition-shadow duration-300 shadow-[inset_0_2px_6px_oklch(0_0_0/0.6),inset_0_0_0_1px_var(--color-ttg09-edge)] has-checked:shadow-[inset_0_2px_6px_oklch(0_0_0/0.6),inset_0_0_0_1px_color-mix(in_oklab,var(--color-ttg09-live)_55%,transparent),0_0_18px_-4px_color-mix(in_oklab,var(--color-ttg09-live)_60%,transparent)]">
<input type="checkbox" id="ttg-09-sw" role="switch" checked class="peer appearance-none absolute inset-0 w-full h-full rounded-[26px] bg-transparent cursor-pointer z-10 focus-visible:outline-3 focus-visible:outline-ttg09-live focus-visible:outline-offset-4">
<span aria-hidden="true" class="absolute inset-0 flex items-center justify-center gap-[5px] pointer-events-none peer-checked:[&_i]:bg-ttg09-live peer-checked:[&_i]:[animation:ttg09-eq_var(--dur,0.9s)_ease-in-out_var(--d)_infinite_alternate] motion-reduce:peer-checked:[&_i]:[animation:none] motion-reduce:peer-checked:[&_i]:scale-y-[0.7] motion-reduce:peer-checked:[&_i:nth-child(odd)]:scale-y-[0.45]">
<i style="--d:0s;" class="w-[5px] h-7 rounded-[3px] bg-ttg09-flat origin-center scale-y-[0.12] transition-[background-color,transform] duration-300"></i>
<i style="--d:.14s; --dur:0.75s;" class="w-[5px] h-7 rounded-[3px] bg-ttg09-flat origin-center scale-y-[0.12] transition-[background-color,transform] duration-300"></i>
<i style="--d:.28s;" class="w-[5px] h-7 rounded-[3px] bg-ttg09-flat origin-center scale-y-[0.12] transition-[background-color,transform] duration-300"></i>
<i style="--d:.07s; --dur:1.05s;" class="w-[5px] h-7 rounded-[3px] bg-ttg09-flat origin-center scale-y-[0.12] transition-[background-color,transform] duration-300"></i>
<i style="--d:.21s; --dur:0.8s;" class="w-[5px] h-7 rounded-[3px] bg-ttg09-flat origin-center scale-y-[0.12] transition-[background-color,transform] duration-300"></i>
</span>
</span>
</label>
<p class="font-ttg09-mono text-[11px] tracking-[0.14em] uppercase text-ttg09-mute">Live = dancing bars · muted = flatline</p>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-ttg09-bg: #0f1216;
--color-ttg09-bg: oklch(0.18 0.008 255);
--color-ttg09-card: #171b21;
--color-ttg09-card: oklch(0.23 0.01 255);
--color-ttg09-edge: #272d36;
--color-ttg09-edge: oklch(0.31 0.012 255);
--color-ttg09-track: #1b2028;
--color-ttg09-track: oklch(0.25 0.012 255);
--color-ttg09-flat: #454e5c;
--color-ttg09-flat: oklch(0.44 0.015 255);
--color-ttg09-live: #3ddc9a;
--color-ttg09-live: oklch(0.82 0.16 165);
--color-ttg09-ink: #d3dae4;
--color-ttg09-ink: oklch(0.89 0.01 255);
--color-ttg09-mute: #8d95a3;
--color-ttg09-mute: oklch(0.66 0.015 255);
--font-ttg09-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--font-ttg09-mono: 'JetBrains Mono', ui-monospace, Menlo, monospace;
--animate-ttg09-eq: ttg09-eq 0.9s ease-in-out infinite alternate;
}
/* The keyframes an --animate-* token refers to still live in plain CSS; v4 mints the
utility from the token, not the block. */
@keyframes ttg09-eq {
0% {
transform: scaleY(0.25);
}
100% {
transform: scaleY(1);
}
}
.ttg-09 {
width: 100%;
min-height: 100svh;
}
.ttg-09 input[type="checkbox"] {
-webkit-appearance: none;
}
```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 Waveform Mute Toggle with Animated EQ Bars
Source: https://codefronts.com/components/tailwind-css-toggles-switches/tailwind-waveform-mute-toggle-animated-eq-bars/
A mute switch that shows sound instead of describing it: five bars dance while audio is live and snap to a flatline the instant you mute. --animate-ttg09-eq in @theme mints the animate-ttg09-eq utility; per-bar delays stay inline, because a stagger is data, not a design token.
## HTML
```html
<section class="ttg-09 w-full min-h-svh flex flex-col items-center justify-center gap-7 px-5 py-14 bg-ttg09-bg font-ttg09-sans" aria-label="Waveform mute toggle">
<label for="ttg-09-sw" class="flex items-center gap-8 px-7 py-5 rounded-3xl cursor-pointer bg-ttg09-card border border-ttg09-edge">
<span class="flex flex-col gap-0.5">
<span class="text-[15.5px] font-semibold text-ttg09-ink">Studio monitor</span>
<span class="text-[12.5px] text-ttg09-mute">Routes to the desk, not the interface</span>
</span>
<span class="relative shrink-0 w-24 h-12 rounded-[26px] bg-ttg09-track transition-shadow duration-300 shadow-[inset_0_2px_6px_oklch(0_0_0/0.6),inset_0_0_0_1px_var(--color-ttg09-edge)] has-checked:shadow-[inset_0_2px_6px_oklch(0_0_0/0.6),inset_0_0_0_1px_color-mix(in_oklab,var(--color-ttg09-live)_55%,transparent),0_0_18px_-4px_color-mix(in_oklab,var(--color-ttg09-live)_60%,transparent)]">
<input type="checkbox" id="ttg-09-sw" role="switch" checked class="peer appearance-none absolute inset-0 w-full h-full rounded-[26px] bg-transparent cursor-pointer z-10 focus-visible:outline-3 focus-visible:outline-ttg09-live focus-visible:outline-offset-4">
<span aria-hidden="true" class="absolute inset-0 flex items-center justify-center gap-[5px] pointer-events-none peer-checked:[&_i]:bg-ttg09-live peer-checked:[&_i]:[animation:ttg09-eq_var(--dur,0.9s)_ease-in-out_var(--d)_infinite_alternate] motion-reduce:peer-checked:[&_i]:[animation:none] motion-reduce:peer-checked:[&_i]:scale-y-[0.7] motion-reduce:peer-checked:[&_i:nth-child(odd)]:scale-y-[0.45]">
<i style="--d:0s;" class="w-[5px] h-7 rounded-[3px] bg-ttg09-flat origin-center scale-y-[0.12] transition-[background-color,transform] duration-300"></i>
<i style="--d:.14s; --dur:0.75s;" class="w-[5px] h-7 rounded-[3px] bg-ttg09-flat origin-center scale-y-[0.12] transition-[background-color,transform] duration-300"></i>
<i style="--d:.28s;" class="w-[5px] h-7 rounded-[3px] bg-ttg09-flat origin-center scale-y-[0.12] transition-[background-color,transform] duration-300"></i>
<i style="--d:.07s; --dur:1.05s;" class="w-[5px] h-7 rounded-[3px] bg-ttg09-flat origin-center scale-y-[0.12] transition-[background-color,transform] duration-300"></i>
<i style="--d:.21s; --dur:0.8s;" class="w-[5px] h-7 rounded-[3px] bg-ttg09-flat origin-center scale-y-[0.12] transition-[background-color,transform] duration-300"></i>
</span>
</span>
</label>
<p class="font-ttg09-mono text-[11px] tracking-[0.14em] uppercase text-ttg09-mute">Live = dancing bars · muted = flatline</p>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-ttg09-bg: #0f1216;
--color-ttg09-bg: oklch(0.18 0.008 255);
--color-ttg09-card: #171b21;
--color-ttg09-card: oklch(0.23 0.01 255);
--color-ttg09-edge: #272d36;
--color-ttg09-edge: oklch(0.31 0.012 255);
--color-ttg09-track: #1b2028;
--color-ttg09-track: oklch(0.25 0.012 255);
--color-ttg09-flat: #454e5c;
--color-ttg09-flat: oklch(0.44 0.015 255);
--color-ttg09-live: #3ddc9a;
--color-ttg09-live: oklch(0.82 0.16 165);
--color-ttg09-ink: #d3dae4;
--color-ttg09-ink: oklch(0.89 0.01 255);
--color-ttg09-mute: #8d95a3;
--color-ttg09-mute: oklch(0.66 0.015 255);
--font-ttg09-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--font-ttg09-mono: 'JetBrains Mono', ui-monospace, Menlo, monospace;
--animate-ttg09-eq: ttg09-eq 0.9s ease-in-out infinite alternate;
}
/* The keyframes an --animate-* token refers to still live in plain CSS; v4 mints the
utility from the token, not the block. */
@keyframes ttg09-eq {
0% {
transform: scaleY(0.25);
}
100% {
transform: scaleY(1);
}
}
.ttg-09 {
width: 100%;
min-height: 100svh;
}
.ttg-09 input[type="checkbox"] {
-webkit-appearance: none;
}
```How this works
Five bars, one keyframe block. Each <i> runs the same scaleY(.25) → scaleY(1) alternating loop, but with a different animation-delay and, on three of them, a different duration. Different periods mean the pattern never visibly repeats — a convincing waveform from five elements and eight lines of CSS.
Muted is animation-none, not paused. Unchecked, the bars carry scale-y-[0.12] and no animation at all, so they snap to a flatline. animation-play-state: paused would freeze them mid-dance at whatever heights they happened to be, which reads as broken rather than muted.
The pill reacts too. has-checked: on the pill adds an accent ring and an outer glow, so live state is carried by colour AND motion AND shape — three signals, which is what lets the control work in greyscale and under reduced motion.
Make it yours
- Add bars: copy an
<i>and give it a new--d; the loop stays organic as long as the delays are not multiples of each other. - Recolour live state through
--color-ttg09-live— bars, ring and glow all read it. - Chill it: raise the animation duration in the
@themeentry to 1.4s. - Skyline instead of EQ: change
origin-centertoorigin-bottomon the bars.
Gotchas — read before shipping
- Stop the loop with
[animation:none], neveranimation-play-state:paused— and note it must also be the shorthand form here, to beat the shorthand that started it. - An infinite animation MUST respect
prefers-reduced-motion. This one swaps to a static two-height pattern, so the live state is still visible — dropping to a flatline would have destroyed the state signal. transform-originis a deliberate design choice here:centerreads as an equaliser,bottomreads as a city skyline. Pick on purpose.- Five infinite compositor animations are cheap, but they are not free on low-end Android — do not scale this to fifty bars.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
Keyframe animations on transform are universal; :has() (Firefox 121+) only drives the pill's ring, and the bars work without it.