32 / 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 Odometer Roll ON / OFF Toggle
The split-flap's mechanical cousin, rebuilt without 3D: a word stack rolls vertically behind a slotted window, with gradient shadows at the top and bottom edges selling the curve of a drum. Cheaper than a 3D flip and it reverses perfectly.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="ttg-32 w-full min-h-svh flex flex-col items-center justify-center gap-7 px-5 py-14 bg-ttg32-bg font-ttg32-mono" aria-label="Odometer roll toggle">
<label for="ttg-32-sw" class="flex flex-col items-center gap-5 cursor-pointer">
<input type="checkbox" id="ttg-32-sw" role="switch" aria-label="Departures feed" class="peer sr-only">
<span aria-hidden="true" class="relative flex items-center gap-4 px-6 py-5 rounded-2xl bg-ttg32-case shadow-[0_18px_36px_-20px_oklch(0_0_0/0.8),inset_0_1px_0_oklch(1_0_0/0.06)] peer-checked:[&_.ttg32-drum]:-translate-y-1/2 peer-checked:[&_.ttg32-status]:-translate-y-1/2 peer-focus-visible:outline-3 peer-focus-visible:outline-ttg32-glyph peer-focus-visible:outline-offset-4">
<span class="relative block w-[132px] h-[62px] overflow-hidden rounded-lg bg-ttg32-slot shadow-[inset_0_2px_6px_oklch(0_0_0/0.7)]">
<span class="ttg32-drum block h-[124px] transition-transform duration-[520ms] ease-[cubic-bezier(0.2,0.9,0.25,1.08)] motion-reduce:duration-150">
<b class="grid place-items-center h-[62px] text-[36px] font-bold tracking-[0.06em] text-ttg32-dim">OFF</b>
<b class="grid place-items-center h-[62px] text-[36px] font-bold tracking-[0.06em] text-ttg32-glyph">ON</b>
</span>
<span class="pointer-events-none absolute inset-x-0 top-0 h-4 bg-linear-to-b from-ttg32-shade to-transparent"></span>
<span class="pointer-events-none absolute inset-x-0 bottom-0 h-4 bg-linear-to-t from-ttg32-shade to-transparent"></span>
</span>
<span class="relative block w-[86px] h-[22px] overflow-hidden">
<span class="ttg32-status block h-11 transition-transform duration-[680ms] ease-[cubic-bezier(0.2,0.9,0.25,1.08)] motion-reduce:duration-150">
<i class="grid place-items-center h-[22px] not-italic text-[11px] tracking-[0.18em] text-ttg32-dim">HOLDING</i>
<i class="grid place-items-center h-[22px] not-italic text-[11px] tracking-[0.18em] text-ttg32-live">RUNNING</i>
</span>
</span>
</span>
<span class="font-ttg32-sans text-[13px] tracking-[0.06em] text-ttg32-label">Departures feed</span>
</label>
<p class="font-ttg32-sans text-[11px] tracking-[0.14em] uppercase text-ttg32-mute">Geared drums · two weights, one state</p>
</section><section class="ttg-32 w-full min-h-svh flex flex-col items-center justify-center gap-7 px-5 py-14 bg-ttg32-bg font-ttg32-mono" aria-label="Odometer roll toggle">
<label for="ttg-32-sw" class="flex flex-col items-center gap-5 cursor-pointer">
<input type="checkbox" id="ttg-32-sw" role="switch" aria-label="Departures feed" class="peer sr-only">
<span aria-hidden="true" class="relative flex items-center gap-4 px-6 py-5 rounded-2xl bg-ttg32-case shadow-[0_18px_36px_-20px_oklch(0_0_0/0.8),inset_0_1px_0_oklch(1_0_0/0.06)] peer-checked:[&_.ttg32-drum]:-translate-y-1/2 peer-checked:[&_.ttg32-status]:-translate-y-1/2 peer-focus-visible:outline-3 peer-focus-visible:outline-ttg32-glyph peer-focus-visible:outline-offset-4">
<span class="relative block w-[132px] h-[62px] overflow-hidden rounded-lg bg-ttg32-slot shadow-[inset_0_2px_6px_oklch(0_0_0/0.7)]">
<span class="ttg32-drum block h-[124px] transition-transform duration-[520ms] ease-[cubic-bezier(0.2,0.9,0.25,1.08)] motion-reduce:duration-150">
<b class="grid place-items-center h-[62px] text-[36px] font-bold tracking-[0.06em] text-ttg32-dim">OFF</b>
<b class="grid place-items-center h-[62px] text-[36px] font-bold tracking-[0.06em] text-ttg32-glyph">ON</b>
</span>
<span class="pointer-events-none absolute inset-x-0 top-0 h-4 bg-linear-to-b from-ttg32-shade to-transparent"></span>
<span class="pointer-events-none absolute inset-x-0 bottom-0 h-4 bg-linear-to-t from-ttg32-shade to-transparent"></span>
</span>
<span class="relative block w-[86px] h-[22px] overflow-hidden">
<span class="ttg32-status block h-11 transition-transform duration-[680ms] ease-[cubic-bezier(0.2,0.9,0.25,1.08)] motion-reduce:duration-150">
<i class="grid place-items-center h-[22px] not-italic text-[11px] tracking-[0.18em] text-ttg32-dim">HOLDING</i>
<i class="grid place-items-center h-[22px] not-italic text-[11px] tracking-[0.18em] text-ttg32-live">RUNNING</i>
</span>
</span>
</span>
<span class="font-ttg32-sans text-[13px] tracking-[0.06em] text-ttg32-label">Departures feed</span>
</label>
<p class="font-ttg32-sans text-[11px] tracking-[0.14em] uppercase text-ttg32-mute">Geared drums · two weights, one state</p>
</section>@import "tailwindcss";
@theme {
--color-ttg32-bg: #14140f;
--color-ttg32-bg: oklch(0.19 0.008 100);
--color-ttg32-case: #26241e;
--color-ttg32-case: oklch(0.27 0.01 85);
--color-ttg32-slot: #171613;
--color-ttg32-slot: oklch(0.2 0.008 85);
--color-ttg32-shade: #0e0d0b;
--color-ttg32-shade: oklch(0.15 0.006 85 / 0.85);
--color-ttg32-glyph: #f7dc84;
--color-ttg32-glyph: oklch(0.89 0.12 92);
--color-ttg32-dim: #9b937a;
--color-ttg32-dim: oklch(0.67 0.035 92);
--color-ttg32-live: #7fdcae;
--color-ttg32-live: oklch(0.83 0.13 165);
--color-ttg32-label: #b6ad93;
--color-ttg32-label: oklch(0.75 0.03 92);
--color-ttg32-mute: #7b7460;
--color-ttg32-mute: oklch(0.56 0.025 92);
--font-ttg32-mono: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, monospace;
--font-ttg32-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
.ttg-32 {
width: 100%;
min-height: 100svh;
}@import "tailwindcss";
@theme {
--color-ttg32-bg: #14140f;
--color-ttg32-bg: oklch(0.19 0.008 100);
--color-ttg32-case: #26241e;
--color-ttg32-case: oklch(0.27 0.01 85);
--color-ttg32-slot: #171613;
--color-ttg32-slot: oklch(0.2 0.008 85);
--color-ttg32-shade: #0e0d0b;
--color-ttg32-shade: oklch(0.15 0.006 85 / 0.85);
--color-ttg32-glyph: #f7dc84;
--color-ttg32-glyph: oklch(0.89 0.12 92);
--color-ttg32-dim: #9b937a;
--color-ttg32-dim: oklch(0.67 0.035 92);
--color-ttg32-live: #7fdcae;
--color-ttg32-live: oklch(0.83 0.13 165);
--color-ttg32-label: #b6ad93;
--color-ttg32-label: oklch(0.75 0.03 92);
--color-ttg32-mute: #7b7460;
--color-ttg32-mute: oklch(0.56 0.025 92);
--font-ttg32-mono: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, monospace;
--font-ttg32-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
.ttg-32 {
width: 100%;
min-height: 100svh;
}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-ttg32-bg: #14140f;
--color-ttg32-bg: oklch(0.19 0.008 100);
--color-ttg32-case: #26241e;
--color-ttg32-case: oklch(0.27 0.01 85);
--color-ttg32-slot: #171613;
--color-ttg32-slot: oklch(0.2 0.008 85);
--color-ttg32-shade: #0e0d0b;
--color-ttg32-shade: oklch(0.15 0.006 85 / 0.85);
--color-ttg32-glyph: #f7dc84;
--color-ttg32-glyph: oklch(0.89 0.12 92);
--color-ttg32-dim: #9b937a;
--color-ttg32-dim: oklch(0.67 0.035 92);
--color-ttg32-live: #7fdcae;
--color-ttg32-live: oklch(0.83 0.13 165);
--color-ttg32-label: #b6ad93;
--color-ttg32-label: oklch(0.75 0.03 92);
--color-ttg32-mute: #7b7460;
--color-ttg32-mute: oklch(0.56 0.025 92);
--font-ttg32-mono: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, monospace;
--font-ttg32-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
.ttg-32 {
width: 100%;
min-height: 100svh;
}@import "tailwindcss";
@theme {
--color-ttg32-bg: #14140f;
--color-ttg32-bg: oklch(0.19 0.008 100);
--color-ttg32-case: #26241e;
--color-ttg32-case: oklch(0.27 0.01 85);
--color-ttg32-slot: #171613;
--color-ttg32-slot: oklch(0.2 0.008 85);
--color-ttg32-shade: #0e0d0b;
--color-ttg32-shade: oklch(0.15 0.006 85 / 0.85);
--color-ttg32-glyph: #f7dc84;
--color-ttg32-glyph: oklch(0.89 0.12 92);
--color-ttg32-dim: #9b937a;
--color-ttg32-dim: oklch(0.67 0.035 92);
--color-ttg32-live: #7fdcae;
--color-ttg32-live: oklch(0.83 0.13 165);
--color-ttg32-label: #b6ad93;
--color-ttg32-label: oklch(0.75 0.03 92);
--color-ttg32-mute: #7b7460;
--color-ttg32-mute: oklch(0.56 0.025 92);
--font-ttg32-mono: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, monospace;
--font-ttg32-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
.ttg-32 {
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=JetBrains+Mono:wght@400;500;700&family=Plus+Jakarta+Sans:wght@400;500;600&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=JetBrains+Mono:wght@400;500;700&family=Plus+Jakarta+Sans:wght@400;500;600&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- Compared with demo 12's split-flap, this is the port that argues for restraint: no
transform-3d, nobackface-hidden, no pre-rotated faces — one-translate-y-1/2and two gradient overlays produce a comparable mechanical read and reverse correctly by construction. - The two drums differ only in
duration-[520ms]versusduration-[680ms]. That 160ms of lag is the whole sense of gearing, and it is the cheapest character in this collection. - Edge shading is two
bg-linear-to-b/to-tspans over a translucent token rather than a box-shadow, because a shadow would clip against the slot's own inset shadow. - UI upgrade over demo 12: a second status drum makes the state readable as a word (HOLDING / RUNNING) rather than only as ON/OFF, the dim tone rose to
oklch(0.67)for 4.6:1 on the slot, and the whole mechanism now survives browsers with no 3D support.
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 Odometer Roll ON / OFF Toggle
Source: https://codefronts.com/components/tailwind-css-toggles-switches/tailwind-odometer-roll-on-off-toggle/
The split-flap's mechanical cousin, rebuilt without 3D: a word stack rolls vertically behind a slotted window, with gradient shadows at the top and bottom edges selling the curve of a drum. Cheaper than a 3D flip and it reverses perfectly.
## HTML
```html
<section class="ttg-32 w-full min-h-svh flex flex-col items-center justify-center gap-7 px-5 py-14 bg-ttg32-bg font-ttg32-mono" aria-label="Odometer roll toggle">
<label for="ttg-32-sw" class="flex flex-col items-center gap-5 cursor-pointer">
<input type="checkbox" id="ttg-32-sw" role="switch" aria-label="Departures feed" class="peer sr-only">
<span aria-hidden="true" class="relative flex items-center gap-4 px-6 py-5 rounded-2xl bg-ttg32-case shadow-[0_18px_36px_-20px_oklch(0_0_0/0.8),inset_0_1px_0_oklch(1_0_0/0.06)] peer-checked:[&_.ttg32-drum]:-translate-y-1/2 peer-checked:[&_.ttg32-status]:-translate-y-1/2 peer-focus-visible:outline-3 peer-focus-visible:outline-ttg32-glyph peer-focus-visible:outline-offset-4">
<span class="relative block w-[132px] h-[62px] overflow-hidden rounded-lg bg-ttg32-slot shadow-[inset_0_2px_6px_oklch(0_0_0/0.7)]">
<span class="ttg32-drum block h-[124px] transition-transform duration-[520ms] ease-[cubic-bezier(0.2,0.9,0.25,1.08)] motion-reduce:duration-150">
<b class="grid place-items-center h-[62px] text-[36px] font-bold tracking-[0.06em] text-ttg32-dim">OFF</b>
<b class="grid place-items-center h-[62px] text-[36px] font-bold tracking-[0.06em] text-ttg32-glyph">ON</b>
</span>
<span class="pointer-events-none absolute inset-x-0 top-0 h-4 bg-linear-to-b from-ttg32-shade to-transparent"></span>
<span class="pointer-events-none absolute inset-x-0 bottom-0 h-4 bg-linear-to-t from-ttg32-shade to-transparent"></span>
</span>
<span class="relative block w-[86px] h-[22px] overflow-hidden">
<span class="ttg32-status block h-11 transition-transform duration-[680ms] ease-[cubic-bezier(0.2,0.9,0.25,1.08)] motion-reduce:duration-150">
<i class="grid place-items-center h-[22px] not-italic text-[11px] tracking-[0.18em] text-ttg32-dim">HOLDING</i>
<i class="grid place-items-center h-[22px] not-italic text-[11px] tracking-[0.18em] text-ttg32-live">RUNNING</i>
</span>
</span>
</span>
<span class="font-ttg32-sans text-[13px] tracking-[0.06em] text-ttg32-label">Departures feed</span>
</label>
<p class="font-ttg32-sans text-[11px] tracking-[0.14em] uppercase text-ttg32-mute">Geared drums · two weights, one state</p>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-ttg32-bg: #14140f;
--color-ttg32-bg: oklch(0.19 0.008 100);
--color-ttg32-case: #26241e;
--color-ttg32-case: oklch(0.27 0.01 85);
--color-ttg32-slot: #171613;
--color-ttg32-slot: oklch(0.2 0.008 85);
--color-ttg32-shade: #0e0d0b;
--color-ttg32-shade: oklch(0.15 0.006 85 / 0.85);
--color-ttg32-glyph: #f7dc84;
--color-ttg32-glyph: oklch(0.89 0.12 92);
--color-ttg32-dim: #9b937a;
--color-ttg32-dim: oklch(0.67 0.035 92);
--color-ttg32-live: #7fdcae;
--color-ttg32-live: oklch(0.83 0.13 165);
--color-ttg32-label: #b6ad93;
--color-ttg32-label: oklch(0.75 0.03 92);
--color-ttg32-mute: #7b7460;
--color-ttg32-mute: oklch(0.56 0.025 92);
--font-ttg32-mono: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, monospace;
--font-ttg32-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
.ttg-32 {
width: 100%;
min-height: 100svh;
}
```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 Odometer Roll ON / OFF Toggle
Source: https://codefronts.com/components/tailwind-css-toggles-switches/tailwind-odometer-roll-on-off-toggle/
The split-flap's mechanical cousin, rebuilt without 3D: a word stack rolls vertically behind a slotted window, with gradient shadows at the top and bottom edges selling the curve of a drum. Cheaper than a 3D flip and it reverses perfectly.
## HTML
```html
<section class="ttg-32 w-full min-h-svh flex flex-col items-center justify-center gap-7 px-5 py-14 bg-ttg32-bg font-ttg32-mono" aria-label="Odometer roll toggle">
<label for="ttg-32-sw" class="flex flex-col items-center gap-5 cursor-pointer">
<input type="checkbox" id="ttg-32-sw" role="switch" aria-label="Departures feed" class="peer sr-only">
<span aria-hidden="true" class="relative flex items-center gap-4 px-6 py-5 rounded-2xl bg-ttg32-case shadow-[0_18px_36px_-20px_oklch(0_0_0/0.8),inset_0_1px_0_oklch(1_0_0/0.06)] peer-checked:[&_.ttg32-drum]:-translate-y-1/2 peer-checked:[&_.ttg32-status]:-translate-y-1/2 peer-focus-visible:outline-3 peer-focus-visible:outline-ttg32-glyph peer-focus-visible:outline-offset-4">
<span class="relative block w-[132px] h-[62px] overflow-hidden rounded-lg bg-ttg32-slot shadow-[inset_0_2px_6px_oklch(0_0_0/0.7)]">
<span class="ttg32-drum block h-[124px] transition-transform duration-[520ms] ease-[cubic-bezier(0.2,0.9,0.25,1.08)] motion-reduce:duration-150">
<b class="grid place-items-center h-[62px] text-[36px] font-bold tracking-[0.06em] text-ttg32-dim">OFF</b>
<b class="grid place-items-center h-[62px] text-[36px] font-bold tracking-[0.06em] text-ttg32-glyph">ON</b>
</span>
<span class="pointer-events-none absolute inset-x-0 top-0 h-4 bg-linear-to-b from-ttg32-shade to-transparent"></span>
<span class="pointer-events-none absolute inset-x-0 bottom-0 h-4 bg-linear-to-t from-ttg32-shade to-transparent"></span>
</span>
<span class="relative block w-[86px] h-[22px] overflow-hidden">
<span class="ttg32-status block h-11 transition-transform duration-[680ms] ease-[cubic-bezier(0.2,0.9,0.25,1.08)] motion-reduce:duration-150">
<i class="grid place-items-center h-[22px] not-italic text-[11px] tracking-[0.18em] text-ttg32-dim">HOLDING</i>
<i class="grid place-items-center h-[22px] not-italic text-[11px] tracking-[0.18em] text-ttg32-live">RUNNING</i>
</span>
</span>
</span>
<span class="font-ttg32-sans text-[13px] tracking-[0.06em] text-ttg32-label">Departures feed</span>
</label>
<p class="font-ttg32-sans text-[11px] tracking-[0.14em] uppercase text-ttg32-mute">Geared drums · two weights, one state</p>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-ttg32-bg: #14140f;
--color-ttg32-bg: oklch(0.19 0.008 100);
--color-ttg32-case: #26241e;
--color-ttg32-case: oklch(0.27 0.01 85);
--color-ttg32-slot: #171613;
--color-ttg32-slot: oklch(0.2 0.008 85);
--color-ttg32-shade: #0e0d0b;
--color-ttg32-shade: oklch(0.15 0.006 85 / 0.85);
--color-ttg32-glyph: #f7dc84;
--color-ttg32-glyph: oklch(0.89 0.12 92);
--color-ttg32-dim: #9b937a;
--color-ttg32-dim: oklch(0.67 0.035 92);
--color-ttg32-live: #7fdcae;
--color-ttg32-live: oklch(0.83 0.13 165);
--color-ttg32-label: #b6ad93;
--color-ttg32-label: oklch(0.75 0.03 92);
--color-ttg32-mute: #7b7460;
--color-ttg32-mute: oklch(0.56 0.025 92);
--font-ttg32-mono: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, monospace;
--font-ttg32-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
.ttg-32 {
width: 100%;
min-height: 100svh;
}
```How this works
A drum is a list plus two shadows. Stack both words in a column twice the window height and translate by 50%. Without the edge gradients it reads as a sliding list; with them the eye supplies the curvature, which is the entire illusion.
Two drums, two speeds. The big ON/OFF drum and the smaller status drum share one state but not one duration — the heavier element settles later, exactly like real geared counters, and that lag is what makes the mechanism feel physical.
It reverses for free. Because this is a transition rather than a keyframe animation, unchecking rolls backwards with no extra code — the practical advantage over the 3D flap in demo 12.
Make it yours
- Three or more positions: make the drum taller and translate by fractions of its height.
- Digits instead of words for a real counter — same markup, ten items.
- Deepen the drum with a stronger edge gradient and a subtle inner shadow.
- Serif or condensed type reads more mechanical than a geometric sans here.
Gotchas — read before shipping
- The window height and the drum's item height must match exactly, or a sliver of the other word shows at rest.
- Keep the edge gradients above the drum in stacking order and
pointer-events-none. - Line-height, not padding, should centre each word — padding breaks the 50% translate maths.
- Under
motion-reduce:shorten the roll rather than removing it; an instant word swap in a drum window reads as a glitch.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
Transforms, overflow and gradients only — no 3D, no :has(), and it degrades to an instant swap if transitions are disabled.