08 / 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 Mechanical Keycap Toggles with Haptic Feedback
A bank of mechanical keycaps that latch: each cap travels down into its socket on press, stays seated and backlit when on, and fires an 8ms navigator.vibrate tick on hardware that supports it. The stem is a solid shadow-[0_7px_0_...] that shortens as the key bottoms out.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="ttg-08 w-full min-h-svh flex flex-col items-center justify-center gap-8 px-5 py-14 bg-ttg08-bg font-ttg08-mono" aria-label="Mechanical keycap toggles">
<div id="ttg-08-board" class="flex gap-5 px-7 py-6 rounded-[22px] bg-linear-160 from-ttg08-boardhi to-ttg08-boardlo shadow-[0_24px_50px_-26px_oklch(0_0_0/0.9),inset_0_1px_0_oklch(1_0_0/0.07)]">
<label class="relative block w-20 h-20 cursor-pointer" style="--glow:oklch(0.78 0.15 220)">
<input type="checkbox" role="switch" checked aria-label="Microphone" class="peer appearance-none absolute inset-[-4px] z-10 rounded-xl cursor-pointer">
<span class="absolute inset-0 flex items-center justify-center rounded-[11px] bg-linear-to-b from-ttg08-caphi to-ttg08-caplo text-ttg08-legend text-[15px] font-bold tracking-[0.12em] shadow-[0_7px_0_var(--color-ttg08-stem),0_12px_18px_-6px_oklch(0_0_0/0.8),inset_0_1px_0_oklch(1_0_0/0.1)] transition-[transform,box-shadow,color,text-shadow] duration-100 ease-out peer-active:translate-y-[5px] peer-active:shadow-[0_2px_0_var(--color-ttg08-stem),0_5px_10px_-4px_oklch(0_0_0/0.8),inset_0_1px_0_oklch(1_0_0/0.08)] peer-checked:translate-y-[3px] peer-checked:shadow-[0_4px_0_var(--color-ttg08-stem),0_8px_14px_-6px_oklch(0_0_0/0.8),inset_0_1px_0_oklch(1_0_0/0.08)] peer-checked:text-(--glow) peer-checked:[text-shadow:0_0_10px_var(--glow),0_0_26px_color-mix(in_oklab,var(--glow)_55%,transparent)] peer-checked:peer-active:translate-y-[5px] peer-focus-visible:outline-3 peer-focus-visible:outline-ttg08-focus peer-focus-visible:outline-offset-4 motion-reduce:transition-[color,text-shadow]">MIC</span>
</label>
<label class="relative block w-20 h-20 cursor-pointer" style="--glow:oklch(0.8 0.16 150)">
<input type="checkbox" role="switch" aria-label="Camera" class="peer appearance-none absolute inset-[-4px] z-10 rounded-xl cursor-pointer">
<span class="absolute inset-0 flex items-center justify-center rounded-[11px] bg-linear-to-b from-ttg08-caphi to-ttg08-caplo text-ttg08-legend text-[15px] font-bold tracking-[0.12em] shadow-[0_7px_0_var(--color-ttg08-stem),0_12px_18px_-6px_oklch(0_0_0/0.8),inset_0_1px_0_oklch(1_0_0/0.1)] transition-[transform,box-shadow,color,text-shadow] duration-100 ease-out peer-active:translate-y-[5px] peer-active:shadow-[0_2px_0_var(--color-ttg08-stem),0_5px_10px_-4px_oklch(0_0_0/0.8),inset_0_1px_0_oklch(1_0_0/0.08)] peer-checked:translate-y-[3px] peer-checked:shadow-[0_4px_0_var(--color-ttg08-stem),0_8px_14px_-6px_oklch(0_0_0/0.8),inset_0_1px_0_oklch(1_0_0/0.08)] peer-checked:text-(--glow) peer-checked:[text-shadow:0_0_10px_var(--glow),0_0_26px_color-mix(in_oklab,var(--glow)_55%,transparent)] peer-checked:peer-active:translate-y-[5px] peer-focus-visible:outline-3 peer-focus-visible:outline-ttg08-focus peer-focus-visible:outline-offset-4 motion-reduce:transition-[color,text-shadow]">CAM</span>
</label>
<label class="relative block w-20 h-20 cursor-pointer" style="--glow:oklch(0.72 0.19 22)">
<input type="checkbox" role="switch" aria-label="Recording" class="peer appearance-none absolute inset-[-4px] z-10 rounded-xl cursor-pointer">
<span class="absolute inset-0 flex items-center justify-center rounded-[11px] bg-linear-to-b from-ttg08-caphi to-ttg08-caplo text-ttg08-legend text-[15px] font-bold tracking-[0.12em] shadow-[0_7px_0_var(--color-ttg08-stem),0_12px_18px_-6px_oklch(0_0_0/0.8),inset_0_1px_0_oklch(1_0_0/0.1)] transition-[transform,box-shadow,color,text-shadow] duration-100 ease-out peer-active:translate-y-[5px] peer-active:shadow-[0_2px_0_var(--color-ttg08-stem),0_5px_10px_-4px_oklch(0_0_0/0.8),inset_0_1px_0_oklch(1_0_0/0.08)] peer-checked:translate-y-[3px] peer-checked:shadow-[0_4px_0_var(--color-ttg08-stem),0_8px_14px_-6px_oklch(0_0_0/0.8),inset_0_1px_0_oklch(1_0_0/0.08)] peer-checked:text-(--glow) peer-checked:[text-shadow:0_0_10px_var(--glow),0_0_26px_color-mix(in_oklab,var(--glow)_55%,transparent)] peer-checked:peer-active:translate-y-[5px] peer-focus-visible:outline-3 peer-focus-visible:outline-ttg08-focus peer-focus-visible:outline-offset-4 motion-reduce:transition-[color,text-shadow]">REC</span>
</label>
</div>
<p class="font-ttg08-sans text-[11px] tracking-[0.14em] uppercase text-ttg08-mute">Latching keys · haptic tick on supported devices</p>
</section><section class="ttg-08 w-full min-h-svh flex flex-col items-center justify-center gap-8 px-5 py-14 bg-ttg08-bg font-ttg08-mono" aria-label="Mechanical keycap toggles">
<div id="ttg-08-board" class="flex gap-5 px-7 py-6 rounded-[22px] bg-linear-160 from-ttg08-boardhi to-ttg08-boardlo shadow-[0_24px_50px_-26px_oklch(0_0_0/0.9),inset_0_1px_0_oklch(1_0_0/0.07)]">
<label class="relative block w-20 h-20 cursor-pointer" style="--glow:oklch(0.78 0.15 220)">
<input type="checkbox" role="switch" checked aria-label="Microphone" class="peer appearance-none absolute inset-[-4px] z-10 rounded-xl cursor-pointer">
<span class="absolute inset-0 flex items-center justify-center rounded-[11px] bg-linear-to-b from-ttg08-caphi to-ttg08-caplo text-ttg08-legend text-[15px] font-bold tracking-[0.12em] shadow-[0_7px_0_var(--color-ttg08-stem),0_12px_18px_-6px_oklch(0_0_0/0.8),inset_0_1px_0_oklch(1_0_0/0.1)] transition-[transform,box-shadow,color,text-shadow] duration-100 ease-out peer-active:translate-y-[5px] peer-active:shadow-[0_2px_0_var(--color-ttg08-stem),0_5px_10px_-4px_oklch(0_0_0/0.8),inset_0_1px_0_oklch(1_0_0/0.08)] peer-checked:translate-y-[3px] peer-checked:shadow-[0_4px_0_var(--color-ttg08-stem),0_8px_14px_-6px_oklch(0_0_0/0.8),inset_0_1px_0_oklch(1_0_0/0.08)] peer-checked:text-(--glow) peer-checked:[text-shadow:0_0_10px_var(--glow),0_0_26px_color-mix(in_oklab,var(--glow)_55%,transparent)] peer-checked:peer-active:translate-y-[5px] peer-focus-visible:outline-3 peer-focus-visible:outline-ttg08-focus peer-focus-visible:outline-offset-4 motion-reduce:transition-[color,text-shadow]">MIC</span>
</label>
<label class="relative block w-20 h-20 cursor-pointer" style="--glow:oklch(0.8 0.16 150)">
<input type="checkbox" role="switch" aria-label="Camera" class="peer appearance-none absolute inset-[-4px] z-10 rounded-xl cursor-pointer">
<span class="absolute inset-0 flex items-center justify-center rounded-[11px] bg-linear-to-b from-ttg08-caphi to-ttg08-caplo text-ttg08-legend text-[15px] font-bold tracking-[0.12em] shadow-[0_7px_0_var(--color-ttg08-stem),0_12px_18px_-6px_oklch(0_0_0/0.8),inset_0_1px_0_oklch(1_0_0/0.1)] transition-[transform,box-shadow,color,text-shadow] duration-100 ease-out peer-active:translate-y-[5px] peer-active:shadow-[0_2px_0_var(--color-ttg08-stem),0_5px_10px_-4px_oklch(0_0_0/0.8),inset_0_1px_0_oklch(1_0_0/0.08)] peer-checked:translate-y-[3px] peer-checked:shadow-[0_4px_0_var(--color-ttg08-stem),0_8px_14px_-6px_oklch(0_0_0/0.8),inset_0_1px_0_oklch(1_0_0/0.08)] peer-checked:text-(--glow) peer-checked:[text-shadow:0_0_10px_var(--glow),0_0_26px_color-mix(in_oklab,var(--glow)_55%,transparent)] peer-checked:peer-active:translate-y-[5px] peer-focus-visible:outline-3 peer-focus-visible:outline-ttg08-focus peer-focus-visible:outline-offset-4 motion-reduce:transition-[color,text-shadow]">CAM</span>
</label>
<label class="relative block w-20 h-20 cursor-pointer" style="--glow:oklch(0.72 0.19 22)">
<input type="checkbox" role="switch" aria-label="Recording" class="peer appearance-none absolute inset-[-4px] z-10 rounded-xl cursor-pointer">
<span class="absolute inset-0 flex items-center justify-center rounded-[11px] bg-linear-to-b from-ttg08-caphi to-ttg08-caplo text-ttg08-legend text-[15px] font-bold tracking-[0.12em] shadow-[0_7px_0_var(--color-ttg08-stem),0_12px_18px_-6px_oklch(0_0_0/0.8),inset_0_1px_0_oklch(1_0_0/0.1)] transition-[transform,box-shadow,color,text-shadow] duration-100 ease-out peer-active:translate-y-[5px] peer-active:shadow-[0_2px_0_var(--color-ttg08-stem),0_5px_10px_-4px_oklch(0_0_0/0.8),inset_0_1px_0_oklch(1_0_0/0.08)] peer-checked:translate-y-[3px] peer-checked:shadow-[0_4px_0_var(--color-ttg08-stem),0_8px_14px_-6px_oklch(0_0_0/0.8),inset_0_1px_0_oklch(1_0_0/0.08)] peer-checked:text-(--glow) peer-checked:[text-shadow:0_0_10px_var(--glow),0_0_26px_color-mix(in_oklab,var(--glow)_55%,transparent)] peer-checked:peer-active:translate-y-[5px] peer-focus-visible:outline-3 peer-focus-visible:outline-ttg08-focus peer-focus-visible:outline-offset-4 motion-reduce:transition-[color,text-shadow]">REC</span>
</label>
</div>
<p class="font-ttg08-sans text-[11px] tracking-[0.14em] uppercase text-ttg08-mute">Latching keys · haptic tick on supported devices</p>
</section>@import "tailwindcss";
@theme {
--color-ttg08-bg: #16181c;
--color-ttg08-bg: oklch(0.2 0.006 265);
--color-ttg08-boardhi: #262a31;
--color-ttg08-boardhi: oklch(0.28 0.008 265);
--color-ttg08-boardlo: #1b1e23;
--color-ttg08-boardlo: oklch(0.23 0.007 265);
--color-ttg08-caphi: #3d424b;
--color-ttg08-caphi: oklch(0.38 0.01 265);
--color-ttg08-caplo: #2c3038;
--color-ttg08-caplo: oklch(0.3 0.009 265);
--color-ttg08-stem: #15161a;
--color-ttg08-stem: oklch(0.19 0.006 265);
--color-ttg08-legend: #9aa1ad;
--color-ttg08-legend: oklch(0.7 0.012 265);
--color-ttg08-mute: #6b7280;
--color-ttg08-mute: oklch(0.56 0.015 265);
--color-ttg08-focus: #7db1ff;
--color-ttg08-focus: oklch(0.76 0.13 250);
--font-ttg08-mono: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, monospace;
--font-ttg08-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
.ttg-08 {
width: 100%;
min-height: 100svh;
}
.ttg-08 input[type="checkbox"] {
-webkit-appearance: none;
}@import "tailwindcss";
@theme {
--color-ttg08-bg: #16181c;
--color-ttg08-bg: oklch(0.2 0.006 265);
--color-ttg08-boardhi: #262a31;
--color-ttg08-boardhi: oklch(0.28 0.008 265);
--color-ttg08-boardlo: #1b1e23;
--color-ttg08-boardlo: oklch(0.23 0.007 265);
--color-ttg08-caphi: #3d424b;
--color-ttg08-caphi: oklch(0.38 0.01 265);
--color-ttg08-caplo: #2c3038;
--color-ttg08-caplo: oklch(0.3 0.009 265);
--color-ttg08-stem: #15161a;
--color-ttg08-stem: oklch(0.19 0.006 265);
--color-ttg08-legend: #9aa1ad;
--color-ttg08-legend: oklch(0.7 0.012 265);
--color-ttg08-mute: #6b7280;
--color-ttg08-mute: oklch(0.56 0.015 265);
--color-ttg08-focus: #7db1ff;
--color-ttg08-focus: oklch(0.76 0.13 250);
--font-ttg08-mono: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, monospace;
--font-ttg08-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
.ttg-08 {
width: 100%;
min-height: 100svh;
}
.ttg-08 input[type="checkbox"] {
-webkit-appearance: none;
}(() => {
// Haptic tick: an 8ms vibration pulse per toggle, on hardware that supports it.
// Everything visual is Tailwind — delete this file and the keys still latch.
const board = document.querySelector('#ttg-08-board');
if (!board) return;
board.addEventListener('change', () => { navigator.vibrate?.(8); });
})();(() => {
// Haptic tick: an 8ms vibration pulse per toggle, on hardware that supports it.
// Everything visual is Tailwind — delete this file and the keys still latch.
const board = document.querySelector('#ttg-08-board');
if (!board) return;
board.addEventListener('change', () => { navigator.vibrate?.(8); });
})();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-ttg08-bg: #16181c;
--color-ttg08-bg: oklch(0.2 0.006 265);
--color-ttg08-boardhi: #262a31;
--color-ttg08-boardhi: oklch(0.28 0.008 265);
--color-ttg08-boardlo: #1b1e23;
--color-ttg08-boardlo: oklch(0.23 0.007 265);
--color-ttg08-caphi: #3d424b;
--color-ttg08-caphi: oklch(0.38 0.01 265);
--color-ttg08-caplo: #2c3038;
--color-ttg08-caplo: oklch(0.3 0.009 265);
--color-ttg08-stem: #15161a;
--color-ttg08-stem: oklch(0.19 0.006 265);
--color-ttg08-legend: #9aa1ad;
--color-ttg08-legend: oklch(0.7 0.012 265);
--color-ttg08-mute: #6b7280;
--color-ttg08-mute: oklch(0.56 0.015 265);
--color-ttg08-focus: #7db1ff;
--color-ttg08-focus: oklch(0.76 0.13 250);
--font-ttg08-mono: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, monospace;
--font-ttg08-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
.ttg-08 {
width: 100%;
min-height: 100svh;
}
.ttg-08 input[type="checkbox"] {
-webkit-appearance: none;
}@import "tailwindcss";
@theme {
--color-ttg08-bg: #16181c;
--color-ttg08-bg: oklch(0.2 0.006 265);
--color-ttg08-boardhi: #262a31;
--color-ttg08-boardhi: oklch(0.28 0.008 265);
--color-ttg08-boardlo: #1b1e23;
--color-ttg08-boardlo: oklch(0.23 0.007 265);
--color-ttg08-caphi: #3d424b;
--color-ttg08-caphi: oklch(0.38 0.01 265);
--color-ttg08-caplo: #2c3038;
--color-ttg08-caplo: oklch(0.3 0.009 265);
--color-ttg08-stem: #15161a;
--color-ttg08-stem: oklch(0.19 0.006 265);
--color-ttg08-legend: #9aa1ad;
--color-ttg08-legend: oklch(0.7 0.012 265);
--color-ttg08-mute: #6b7280;
--color-ttg08-mute: oklch(0.56 0.015 265);
--color-ttg08-focus: #7db1ff;
--color-ttg08-focus: oklch(0.76 0.13 250);
--font-ttg08-mono: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, monospace;
--font-ttg08-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
.ttg-08 {
width: 100%;
min-height: 100svh;
}
.ttg-08 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=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.
- The cap is a sibling
<span>rather than a::before, purely sopeer-checked:andpeer-active:can reach it — pseudo-elements of an input cannot be styled from the input's own state variants in a readable way once four states are involved. peer-checked:peer-active:translate-y-[5px]stacks two peer variants on one element, which v4 handles by chaining both conditions onto the same.peer:checked:active ~ &selector. That compound is what stops a latched key from jumping back up when pressed.text-(--glow)is v4's shorthand fortext-[var(--glow)], and it is what lets three keys share one class list and differ by a single inline property.- UI change from the CSS original: the legend ink rose from
#8b919dtooklch(0.7 0.012 265)so an unlit key still reads at 4.6:1 on the cap, and the board gained a 160° gradient plus a deeper drop so the bank looks machined rather than drawn.
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 Mechanical Keycap Toggles with Haptic Feedback
Source: https://codefronts.com/components/tailwind-css-toggles-switches/tailwind-mechanical-keycap-toggles-haptic-feedback/
A bank of mechanical keycaps that latch: each cap travels down into its socket on press, stays seated and backlit when on, and fires an 8ms navigator.vibrate tick on hardware that supports it. The stem is a solid shadow-[0_7px_0_...] that shortens as the key bottoms out.
## HTML
```html
<section class="ttg-08 w-full min-h-svh flex flex-col items-center justify-center gap-8 px-5 py-14 bg-ttg08-bg font-ttg08-mono" aria-label="Mechanical keycap toggles">
<div id="ttg-08-board" class="flex gap-5 px-7 py-6 rounded-[22px] bg-linear-160 from-ttg08-boardhi to-ttg08-boardlo shadow-[0_24px_50px_-26px_oklch(0_0_0/0.9),inset_0_1px_0_oklch(1_0_0/0.07)]">
<label class="relative block w-20 h-20 cursor-pointer" style="--glow:oklch(0.78 0.15 220)">
<input type="checkbox" role="switch" checked aria-label="Microphone" class="peer appearance-none absolute inset-[-4px] z-10 rounded-xl cursor-pointer">
<span class="absolute inset-0 flex items-center justify-center rounded-[11px] bg-linear-to-b from-ttg08-caphi to-ttg08-caplo text-ttg08-legend text-[15px] font-bold tracking-[0.12em] shadow-[0_7px_0_var(--color-ttg08-stem),0_12px_18px_-6px_oklch(0_0_0/0.8),inset_0_1px_0_oklch(1_0_0/0.1)] transition-[transform,box-shadow,color,text-shadow] duration-100 ease-out peer-active:translate-y-[5px] peer-active:shadow-[0_2px_0_var(--color-ttg08-stem),0_5px_10px_-4px_oklch(0_0_0/0.8),inset_0_1px_0_oklch(1_0_0/0.08)] peer-checked:translate-y-[3px] peer-checked:shadow-[0_4px_0_var(--color-ttg08-stem),0_8px_14px_-6px_oklch(0_0_0/0.8),inset_0_1px_0_oklch(1_0_0/0.08)] peer-checked:text-(--glow) peer-checked:[text-shadow:0_0_10px_var(--glow),0_0_26px_color-mix(in_oklab,var(--glow)_55%,transparent)] peer-checked:peer-active:translate-y-[5px] peer-focus-visible:outline-3 peer-focus-visible:outline-ttg08-focus peer-focus-visible:outline-offset-4 motion-reduce:transition-[color,text-shadow]">MIC</span>
</label>
<label class="relative block w-20 h-20 cursor-pointer" style="--glow:oklch(0.8 0.16 150)">
<input type="checkbox" role="switch" aria-label="Camera" class="peer appearance-none absolute inset-[-4px] z-10 rounded-xl cursor-pointer">
<span class="absolute inset-0 flex items-center justify-center rounded-[11px] bg-linear-to-b from-ttg08-caphi to-ttg08-caplo text-ttg08-legend text-[15px] font-bold tracking-[0.12em] shadow-[0_7px_0_var(--color-ttg08-stem),0_12px_18px_-6px_oklch(0_0_0/0.8),inset_0_1px_0_oklch(1_0_0/0.1)] transition-[transform,box-shadow,color,text-shadow] duration-100 ease-out peer-active:translate-y-[5px] peer-active:shadow-[0_2px_0_var(--color-ttg08-stem),0_5px_10px_-4px_oklch(0_0_0/0.8),inset_0_1px_0_oklch(1_0_0/0.08)] peer-checked:translate-y-[3px] peer-checked:shadow-[0_4px_0_var(--color-ttg08-stem),0_8px_14px_-6px_oklch(0_0_0/0.8),inset_0_1px_0_oklch(1_0_0/0.08)] peer-checked:text-(--glow) peer-checked:[text-shadow:0_0_10px_var(--glow),0_0_26px_color-mix(in_oklab,var(--glow)_55%,transparent)] peer-checked:peer-active:translate-y-[5px] peer-focus-visible:outline-3 peer-focus-visible:outline-ttg08-focus peer-focus-visible:outline-offset-4 motion-reduce:transition-[color,text-shadow]">CAM</span>
</label>
<label class="relative block w-20 h-20 cursor-pointer" style="--glow:oklch(0.72 0.19 22)">
<input type="checkbox" role="switch" aria-label="Recording" class="peer appearance-none absolute inset-[-4px] z-10 rounded-xl cursor-pointer">
<span class="absolute inset-0 flex items-center justify-center rounded-[11px] bg-linear-to-b from-ttg08-caphi to-ttg08-caplo text-ttg08-legend text-[15px] font-bold tracking-[0.12em] shadow-[0_7px_0_var(--color-ttg08-stem),0_12px_18px_-6px_oklch(0_0_0/0.8),inset_0_1px_0_oklch(1_0_0/0.1)] transition-[transform,box-shadow,color,text-shadow] duration-100 ease-out peer-active:translate-y-[5px] peer-active:shadow-[0_2px_0_var(--color-ttg08-stem),0_5px_10px_-4px_oklch(0_0_0/0.8),inset_0_1px_0_oklch(1_0_0/0.08)] peer-checked:translate-y-[3px] peer-checked:shadow-[0_4px_0_var(--color-ttg08-stem),0_8px_14px_-6px_oklch(0_0_0/0.8),inset_0_1px_0_oklch(1_0_0/0.08)] peer-checked:text-(--glow) peer-checked:[text-shadow:0_0_10px_var(--glow),0_0_26px_color-mix(in_oklab,var(--glow)_55%,transparent)] peer-checked:peer-active:translate-y-[5px] peer-focus-visible:outline-3 peer-focus-visible:outline-ttg08-focus peer-focus-visible:outline-offset-4 motion-reduce:transition-[color,text-shadow]">REC</span>
</label>
</div>
<p class="font-ttg08-sans text-[11px] tracking-[0.14em] uppercase text-ttg08-mute">Latching keys · haptic tick on supported devices</p>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-ttg08-bg: #16181c;
--color-ttg08-bg: oklch(0.2 0.006 265);
--color-ttg08-boardhi: #262a31;
--color-ttg08-boardhi: oklch(0.28 0.008 265);
--color-ttg08-boardlo: #1b1e23;
--color-ttg08-boardlo: oklch(0.23 0.007 265);
--color-ttg08-caphi: #3d424b;
--color-ttg08-caphi: oklch(0.38 0.01 265);
--color-ttg08-caplo: #2c3038;
--color-ttg08-caplo: oklch(0.3 0.009 265);
--color-ttg08-stem: #15161a;
--color-ttg08-stem: oklch(0.19 0.006 265);
--color-ttg08-legend: #9aa1ad;
--color-ttg08-legend: oklch(0.7 0.012 265);
--color-ttg08-mute: #6b7280;
--color-ttg08-mute: oklch(0.56 0.015 265);
--color-ttg08-focus: #7db1ff;
--color-ttg08-focus: oklch(0.76 0.13 250);
--font-ttg08-mono: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, monospace;
--font-ttg08-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
.ttg-08 {
width: 100%;
min-height: 100svh;
}
.ttg-08 input[type="checkbox"] {
-webkit-appearance: none;
}
```
## JavaScript
```js
(() => {
// Haptic tick: an 8ms vibration pulse per toggle, on hardware that supports it.
// Everything visual is Tailwind — delete this file and the keys still latch.
const board = document.querySelector('#ttg-08-board');
if (!board) return;
board.addEventListener('change', () => { navigator.vibrate?.(8); });
})();
```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 Mechanical Keycap Toggles with Haptic Feedback
Source: https://codefronts.com/components/tailwind-css-toggles-switches/tailwind-mechanical-keycap-toggles-haptic-feedback/
A bank of mechanical keycaps that latch: each cap travels down into its socket on press, stays seated and backlit when on, and fires an 8ms navigator.vibrate tick on hardware that supports it. The stem is a solid shadow-[0_7px_0_...] that shortens as the key bottoms out.
## HTML
```html
<section class="ttg-08 w-full min-h-svh flex flex-col items-center justify-center gap-8 px-5 py-14 bg-ttg08-bg font-ttg08-mono" aria-label="Mechanical keycap toggles">
<div id="ttg-08-board" class="flex gap-5 px-7 py-6 rounded-[22px] bg-linear-160 from-ttg08-boardhi to-ttg08-boardlo shadow-[0_24px_50px_-26px_oklch(0_0_0/0.9),inset_0_1px_0_oklch(1_0_0/0.07)]">
<label class="relative block w-20 h-20 cursor-pointer" style="--glow:oklch(0.78 0.15 220)">
<input type="checkbox" role="switch" checked aria-label="Microphone" class="peer appearance-none absolute inset-[-4px] z-10 rounded-xl cursor-pointer">
<span class="absolute inset-0 flex items-center justify-center rounded-[11px] bg-linear-to-b from-ttg08-caphi to-ttg08-caplo text-ttg08-legend text-[15px] font-bold tracking-[0.12em] shadow-[0_7px_0_var(--color-ttg08-stem),0_12px_18px_-6px_oklch(0_0_0/0.8),inset_0_1px_0_oklch(1_0_0/0.1)] transition-[transform,box-shadow,color,text-shadow] duration-100 ease-out peer-active:translate-y-[5px] peer-active:shadow-[0_2px_0_var(--color-ttg08-stem),0_5px_10px_-4px_oklch(0_0_0/0.8),inset_0_1px_0_oklch(1_0_0/0.08)] peer-checked:translate-y-[3px] peer-checked:shadow-[0_4px_0_var(--color-ttg08-stem),0_8px_14px_-6px_oklch(0_0_0/0.8),inset_0_1px_0_oklch(1_0_0/0.08)] peer-checked:text-(--glow) peer-checked:[text-shadow:0_0_10px_var(--glow),0_0_26px_color-mix(in_oklab,var(--glow)_55%,transparent)] peer-checked:peer-active:translate-y-[5px] peer-focus-visible:outline-3 peer-focus-visible:outline-ttg08-focus peer-focus-visible:outline-offset-4 motion-reduce:transition-[color,text-shadow]">MIC</span>
</label>
<label class="relative block w-20 h-20 cursor-pointer" style="--glow:oklch(0.8 0.16 150)">
<input type="checkbox" role="switch" aria-label="Camera" class="peer appearance-none absolute inset-[-4px] z-10 rounded-xl cursor-pointer">
<span class="absolute inset-0 flex items-center justify-center rounded-[11px] bg-linear-to-b from-ttg08-caphi to-ttg08-caplo text-ttg08-legend text-[15px] font-bold tracking-[0.12em] shadow-[0_7px_0_var(--color-ttg08-stem),0_12px_18px_-6px_oklch(0_0_0/0.8),inset_0_1px_0_oklch(1_0_0/0.1)] transition-[transform,box-shadow,color,text-shadow] duration-100 ease-out peer-active:translate-y-[5px] peer-active:shadow-[0_2px_0_var(--color-ttg08-stem),0_5px_10px_-4px_oklch(0_0_0/0.8),inset_0_1px_0_oklch(1_0_0/0.08)] peer-checked:translate-y-[3px] peer-checked:shadow-[0_4px_0_var(--color-ttg08-stem),0_8px_14px_-6px_oklch(0_0_0/0.8),inset_0_1px_0_oklch(1_0_0/0.08)] peer-checked:text-(--glow) peer-checked:[text-shadow:0_0_10px_var(--glow),0_0_26px_color-mix(in_oklab,var(--glow)_55%,transparent)] peer-checked:peer-active:translate-y-[5px] peer-focus-visible:outline-3 peer-focus-visible:outline-ttg08-focus peer-focus-visible:outline-offset-4 motion-reduce:transition-[color,text-shadow]">CAM</span>
</label>
<label class="relative block w-20 h-20 cursor-pointer" style="--glow:oklch(0.72 0.19 22)">
<input type="checkbox" role="switch" aria-label="Recording" class="peer appearance-none absolute inset-[-4px] z-10 rounded-xl cursor-pointer">
<span class="absolute inset-0 flex items-center justify-center rounded-[11px] bg-linear-to-b from-ttg08-caphi to-ttg08-caplo text-ttg08-legend text-[15px] font-bold tracking-[0.12em] shadow-[0_7px_0_var(--color-ttg08-stem),0_12px_18px_-6px_oklch(0_0_0/0.8),inset_0_1px_0_oklch(1_0_0/0.1)] transition-[transform,box-shadow,color,text-shadow] duration-100 ease-out peer-active:translate-y-[5px] peer-active:shadow-[0_2px_0_var(--color-ttg08-stem),0_5px_10px_-4px_oklch(0_0_0/0.8),inset_0_1px_0_oklch(1_0_0/0.08)] peer-checked:translate-y-[3px] peer-checked:shadow-[0_4px_0_var(--color-ttg08-stem),0_8px_14px_-6px_oklch(0_0_0/0.8),inset_0_1px_0_oklch(1_0_0/0.08)] peer-checked:text-(--glow) peer-checked:[text-shadow:0_0_10px_var(--glow),0_0_26px_color-mix(in_oklab,var(--glow)_55%,transparent)] peer-checked:peer-active:translate-y-[5px] peer-focus-visible:outline-3 peer-focus-visible:outline-ttg08-focus peer-focus-visible:outline-offset-4 motion-reduce:transition-[color,text-shadow]">REC</span>
</label>
</div>
<p class="font-ttg08-sans text-[11px] tracking-[0.14em] uppercase text-ttg08-mute">Latching keys · haptic tick on supported devices</p>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-ttg08-bg: #16181c;
--color-ttg08-bg: oklch(0.2 0.006 265);
--color-ttg08-boardhi: #262a31;
--color-ttg08-boardhi: oklch(0.28 0.008 265);
--color-ttg08-boardlo: #1b1e23;
--color-ttg08-boardlo: oklch(0.23 0.007 265);
--color-ttg08-caphi: #3d424b;
--color-ttg08-caphi: oklch(0.38 0.01 265);
--color-ttg08-caplo: #2c3038;
--color-ttg08-caplo: oklch(0.3 0.009 265);
--color-ttg08-stem: #15161a;
--color-ttg08-stem: oklch(0.19 0.006 265);
--color-ttg08-legend: #9aa1ad;
--color-ttg08-legend: oklch(0.7 0.012 265);
--color-ttg08-mute: #6b7280;
--color-ttg08-mute: oklch(0.56 0.015 265);
--color-ttg08-focus: #7db1ff;
--color-ttg08-focus: oklch(0.76 0.13 250);
--font-ttg08-mono: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, monospace;
--font-ttg08-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
.ttg-08 {
width: 100%;
min-height: 100svh;
}
.ttg-08 input[type="checkbox"] {
-webkit-appearance: none;
}
```
## JavaScript
```js
(() => {
// Haptic tick: an 8ms vibration pulse per toggle, on hardware that supports it.
// Everything visual is Tailwind — delete this file and the keys still latch.
const board = document.querySelector('#ttg-08-board');
if (!board) return;
board.addEventListener('change', () => { navigator.vibrate?.(8); });
})();
```How this works
Depth is one shadow. A solid, un-blurred 0 7px 0 shadow under the cap reads as the key's side wall; a second blurred shadow grounds it on the board. active: translates the cap down 5px and cuts the stem to 2px, so the cap appears to bottom out rather than slide.
Three positions, not two. Resting, latched (translate-y-[3px], stem 4px, legend lit) and pressed (translate-y-[5px], stem 2px). The latched middle position is what makes the bank readable at a glance — an on key physically sits lower than an off key, so state survives greyscale and colour blindness both.
JavaScript does exactly one thing. A single delegated change listener calls navigator.vibrate?.(8). The visual toggle is entirely Tailwind and works with the script deleted.
Make it yours
- Retint each key by editing its inline
--glow; the legend colour, the bloom and the halo all read it. - Deeper travel: raise the stem to
0_9px_0and the pressed translate to7px. - Heavier thock:
navigator.vibrate(16)— anything past ~25ms starts to feel like an error buzz. - Add keys by copying a label; the board is a flex row and needs no other change.
Gotchas — read before shipping
navigator.vibraterequires a user gesture, is Android-only in practice and is a no-op on iOS. Always call it with optional chaining and never gate UI state on it.- The input must cover the cap's full travel range (
inset-[-4px]here), or a click at the bottom of a pressed key lands on the board instead. - Keep the press on
transformandbox-shadow. Animatingtopormarginrelayouts the whole board on every keystroke. - A backlit legend is a colour-only signal on its own — that is why the latched key also sits lower.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ (no vibration) | 128+ (vibration Android only) | 111+ |
The key mechanics are pure CSS and work everywhere; the Vibration API is a progressive enhancement that iOS Safari ignores without error.