01 / 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 iOS-Style Toggle Switch with appearance-none
The most searched toggle on earth, ported: appearance-none turns the checkbox itself into the 52×32 track, a before: thumb glides across on checked:before:translate-x-5, and active:before:w-8 reproduces the native iOS squash. The port's win is that the resting, on, pressed and reduced-motion states all sit in one class list on the input.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="ttg-01 w-full min-h-svh flex flex-col items-center justify-center gap-6 px-5 py-14 bg-ttg01-bg font-ttg01-sans" aria-label="iOS-style toggle switch">
<div class="w-[min(360px,100%)] rounded-3xl bg-white border border-ttg01-edge shadow-[0_1px_2px_oklch(0.55_0.02_255/0.06),0_18px_40px_-28px_oklch(0.45_0.05_255/0.45)] divide-y divide-ttg01-edge overflow-hidden">
<label for="ttg-01-sw" class="flex items-center gap-6 min-h-16 px-5 py-4 cursor-pointer transition-colors duration-150 hover:bg-ttg01-tint">
<span class="flex flex-col gap-0.5 flex-1">
<span class="text-[15.5px] font-semibold text-ttg01-ink">Wi-Fi</span>
<span class="text-[12.5px] text-ttg01-mute">Auto-join known networks</span>
</span>
<input type="checkbox" id="ttg-01-sw" role="switch" checked class="appearance-none shrink-0 w-[52px] h-8 rounded-full bg-ttg01-off relative cursor-pointer transition-colors duration-300 shadow-[inset_0_0_0_1px_oklch(0.55_0.02_255/0.08)] before:content-[''] before:absolute before:top-0.5 before:left-0.5 before:w-7 before:h-7 before:rounded-full before:bg-white before:shadow-[0_3px_8px_oklch(0.3_0.02_255/0.18),0_1px_1px_oklch(0.3_0.02_255/0.16)] before:transition-[transform,width] before:duration-300 before:ease-[cubic-bezier(0.2,0.9,0.25,1.05)] checked:bg-ttg01-on checked:before:translate-x-5 active:before:w-8 checked:active:before:translate-x-[15px] focus-visible:outline-3 focus-visible:outline-ttg01-focus focus-visible:outline-offset-3 motion-reduce:transition-none motion-reduce:before:transition-none">
</label>
<label for="ttg-01-bt" class="flex items-center gap-6 min-h-16 px-5 py-4 cursor-pointer transition-colors duration-150 hover:bg-ttg01-tint">
<span class="flex flex-col gap-0.5 flex-1">
<span class="text-[15.5px] font-semibold text-ttg01-ink">Bluetooth</span>
<span class="text-[12.5px] text-ttg01-mute">Discoverable as “Studio”</span>
</span>
<input type="checkbox" id="ttg-01-bt" role="switch" class="appearance-none shrink-0 w-[52px] h-8 rounded-full bg-ttg01-off relative cursor-pointer transition-colors duration-300 shadow-[inset_0_0_0_1px_oklch(0.55_0.02_255/0.08)] before:content-[''] before:absolute before:top-0.5 before:left-0.5 before:w-7 before:h-7 before:rounded-full before:bg-white before:shadow-[0_3px_8px_oklch(0.3_0.02_255/0.18),0_1px_1px_oklch(0.3_0.02_255/0.16)] before:transition-[transform,width] before:duration-300 before:ease-[cubic-bezier(0.2,0.9,0.25,1.05)] checked:bg-ttg01-on checked:before:translate-x-5 active:before:w-8 checked:active:before:translate-x-[15px] focus-visible:outline-3 focus-visible:outline-ttg01-focus focus-visible:outline-offset-3 motion-reduce:transition-none motion-reduce:before:transition-none">
</label>
</div>
<p class="font-ttg01-mono text-[11px] tracking-[0.14em] uppercase text-ttg01-mute">Click · or Tab + Space</p>
</section><section class="ttg-01 w-full min-h-svh flex flex-col items-center justify-center gap-6 px-5 py-14 bg-ttg01-bg font-ttg01-sans" aria-label="iOS-style toggle switch">
<div class="w-[min(360px,100%)] rounded-3xl bg-white border border-ttg01-edge shadow-[0_1px_2px_oklch(0.55_0.02_255/0.06),0_18px_40px_-28px_oklch(0.45_0.05_255/0.45)] divide-y divide-ttg01-edge overflow-hidden">
<label for="ttg-01-sw" class="flex items-center gap-6 min-h-16 px-5 py-4 cursor-pointer transition-colors duration-150 hover:bg-ttg01-tint">
<span class="flex flex-col gap-0.5 flex-1">
<span class="text-[15.5px] font-semibold text-ttg01-ink">Wi-Fi</span>
<span class="text-[12.5px] text-ttg01-mute">Auto-join known networks</span>
</span>
<input type="checkbox" id="ttg-01-sw" role="switch" checked class="appearance-none shrink-0 w-[52px] h-8 rounded-full bg-ttg01-off relative cursor-pointer transition-colors duration-300 shadow-[inset_0_0_0_1px_oklch(0.55_0.02_255/0.08)] before:content-[''] before:absolute before:top-0.5 before:left-0.5 before:w-7 before:h-7 before:rounded-full before:bg-white before:shadow-[0_3px_8px_oklch(0.3_0.02_255/0.18),0_1px_1px_oklch(0.3_0.02_255/0.16)] before:transition-[transform,width] before:duration-300 before:ease-[cubic-bezier(0.2,0.9,0.25,1.05)] checked:bg-ttg01-on checked:before:translate-x-5 active:before:w-8 checked:active:before:translate-x-[15px] focus-visible:outline-3 focus-visible:outline-ttg01-focus focus-visible:outline-offset-3 motion-reduce:transition-none motion-reduce:before:transition-none">
</label>
<label for="ttg-01-bt" class="flex items-center gap-6 min-h-16 px-5 py-4 cursor-pointer transition-colors duration-150 hover:bg-ttg01-tint">
<span class="flex flex-col gap-0.5 flex-1">
<span class="text-[15.5px] font-semibold text-ttg01-ink">Bluetooth</span>
<span class="text-[12.5px] text-ttg01-mute">Discoverable as “Studio”</span>
</span>
<input type="checkbox" id="ttg-01-bt" role="switch" class="appearance-none shrink-0 w-[52px] h-8 rounded-full bg-ttg01-off relative cursor-pointer transition-colors duration-300 shadow-[inset_0_0_0_1px_oklch(0.55_0.02_255/0.08)] before:content-[''] before:absolute before:top-0.5 before:left-0.5 before:w-7 before:h-7 before:rounded-full before:bg-white before:shadow-[0_3px_8px_oklch(0.3_0.02_255/0.18),0_1px_1px_oklch(0.3_0.02_255/0.16)] before:transition-[transform,width] before:duration-300 before:ease-[cubic-bezier(0.2,0.9,0.25,1.05)] checked:bg-ttg01-on checked:before:translate-x-5 active:before:w-8 checked:active:before:translate-x-[15px] focus-visible:outline-3 focus-visible:outline-ttg01-focus focus-visible:outline-offset-3 motion-reduce:transition-none motion-reduce:before:transition-none">
</label>
</div>
<p class="font-ttg01-mono text-[11px] tracking-[0.14em] uppercase text-ttg01-mute">Click · or Tab + Space</p>
</section>@import "tailwindcss";
@theme {
--color-ttg01-bg: #f1f2f6;
--color-ttg01-bg: oklch(0.96 0.005 255);
--color-ttg01-edge: #e6e7ee;
--color-ttg01-edge: oklch(0.93 0.005 255);
--color-ttg01-tint: #f7f8fb;
--color-ttg01-tint: oklch(0.98 0.004 255);
--color-ttg01-ink: #1c1d22;
--color-ttg01-ink: oklch(0.24 0.008 265);
--color-ttg01-mute: #8b8e99;
--color-ttg01-mute: oklch(0.63 0.012 265);
--color-ttg01-off: #e4e4ea;
--color-ttg01-off: oklch(0.92 0.004 265);
--color-ttg01-on: #2fbf5b;
--color-ttg01-on: oklch(0.72 0.18 149);
--color-ttg01-focus: #0a84ff;
--color-ttg01-focus: oklch(0.62 0.2 256);
--font-ttg01-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--font-ttg01-mono: 'JetBrains Mono', ui-monospace, Menlo, monospace;
}
/* Root rule: the full-bleed contract, also stated in the markup's utilities, so the
stage is sized correctly before the browser build has compiled. */
.ttg-01 {
width: 100%;
min-height: 100svh;
}
/* Older Safari still wants the prefix on a checkbox. */
.ttg-01 input[type="checkbox"] {
-webkit-appearance: none;
}@import "tailwindcss";
@theme {
--color-ttg01-bg: #f1f2f6;
--color-ttg01-bg: oklch(0.96 0.005 255);
--color-ttg01-edge: #e6e7ee;
--color-ttg01-edge: oklch(0.93 0.005 255);
--color-ttg01-tint: #f7f8fb;
--color-ttg01-tint: oklch(0.98 0.004 255);
--color-ttg01-ink: #1c1d22;
--color-ttg01-ink: oklch(0.24 0.008 265);
--color-ttg01-mute: #8b8e99;
--color-ttg01-mute: oklch(0.63 0.012 265);
--color-ttg01-off: #e4e4ea;
--color-ttg01-off: oklch(0.92 0.004 265);
--color-ttg01-on: #2fbf5b;
--color-ttg01-on: oklch(0.72 0.18 149);
--color-ttg01-focus: #0a84ff;
--color-ttg01-focus: oklch(0.62 0.2 256);
--font-ttg01-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--font-ttg01-mono: 'JetBrains Mono', ui-monospace, Menlo, monospace;
}
/* Root rule: the full-bleed contract, also stated in the markup's utilities, so the
stage is sized correctly before the browser build has compiled. */
.ttg-01 {
width: 100%;
min-height: 100svh;
}
/* Older Safari still wants the prefix on a checkbox. */
.ttg-01 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-ttg01-bg: #f1f2f6;
--color-ttg01-bg: oklch(0.96 0.005 255);
--color-ttg01-edge: #e6e7ee;
--color-ttg01-edge: oklch(0.93 0.005 255);
--color-ttg01-tint: #f7f8fb;
--color-ttg01-tint: oklch(0.98 0.004 255);
--color-ttg01-ink: #1c1d22;
--color-ttg01-ink: oklch(0.24 0.008 265);
--color-ttg01-mute: #8b8e99;
--color-ttg01-mute: oklch(0.63 0.012 265);
--color-ttg01-off: #e4e4ea;
--color-ttg01-off: oklch(0.92 0.004 265);
--color-ttg01-on: #2fbf5b;
--color-ttg01-on: oklch(0.72 0.18 149);
--color-ttg01-focus: #0a84ff;
--color-ttg01-focus: oklch(0.62 0.2 256);
--font-ttg01-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--font-ttg01-mono: 'JetBrains Mono', ui-monospace, Menlo, monospace;
}
/* Root rule: the full-bleed contract, also stated in the markup's utilities, so the
stage is sized correctly before the browser build has compiled. */
.ttg-01 {
width: 100%;
min-height: 100svh;
}
/* Older Safari still wants the prefix on a checkbox. */
.ttg-01 input[type="checkbox"] {
-webkit-appearance: none;
}@import "tailwindcss";
@theme {
--color-ttg01-bg: #f1f2f6;
--color-ttg01-bg: oklch(0.96 0.005 255);
--color-ttg01-edge: #e6e7ee;
--color-ttg01-edge: oklch(0.93 0.005 255);
--color-ttg01-tint: #f7f8fb;
--color-ttg01-tint: oklch(0.98 0.004 255);
--color-ttg01-ink: #1c1d22;
--color-ttg01-ink: oklch(0.24 0.008 265);
--color-ttg01-mute: #8b8e99;
--color-ttg01-mute: oklch(0.63 0.012 265);
--color-ttg01-off: #e4e4ea;
--color-ttg01-off: oklch(0.92 0.004 265);
--color-ttg01-on: #2fbf5b;
--color-ttg01-on: oklch(0.72 0.18 149);
--color-ttg01-focus: #0a84ff;
--color-ttg01-focus: oklch(0.62 0.2 256);
--font-ttg01-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--font-ttg01-mono: 'JetBrains Mono', ui-monospace, Menlo, monospace;
}
/* Root rule: the full-bleed contract, also stated in the markup's utilities, so the
stage is sized correctly before the browser build has compiled. */
.ttg-01 {
width: 100%;
min-height: 100svh;
}
/* Older Safari still wants the prefix on a checkbox. */
.ttg-01 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.
.tgl-01__sw::beforeand its four state rules collapse into one class list:before:*for the resting thumb,checked:before:translate-x-5,active:before:w-8and the compoundchecked:active:before:translate-x-[15px]. The travel number now sits beside the track width that produced it.-webkit-appearance:nonestays in CSS — Tailwind'sappearance-noneemits the unprefixed property only, and older Safari needs the prefix on checkboxes specifically.motion-reduce:replaces the media query verbatim, which is the cleanest single win in this whole port: the fallback is authored on the element it protects.- UI change from the CSS original: one bare row became a two-row iOS-18-style settings card with hairline dividers, a hover tint and 64px rows (past the 44px target floor), and the on-green moved from
#34c759tooklch(0.72 0.18 149)— same hue family, but it holds 3:1 against white at the track edge, which the original green did not.
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 iOS-Style Toggle Switch with appearance-none
Source: https://codefronts.com/components/tailwind-css-toggles-switches/tailwind-ios-style-toggle-switch-appearance-none/
The most searched toggle on earth, ported: appearance-none turns the checkbox itself into the 52×32 track, a before: thumb glides across on checked:before:translate-x-5, and active:before:w-8 reproduces the native iOS squash. The port's win is that the resting, on, pressed and reduced-motion states all sit in one class list on the input.
## HTML
```html
<section class="ttg-01 w-full min-h-svh flex flex-col items-center justify-center gap-6 px-5 py-14 bg-ttg01-bg font-ttg01-sans" aria-label="iOS-style toggle switch">
<div class="w-[min(360px,100%)] rounded-3xl bg-white border border-ttg01-edge shadow-[0_1px_2px_oklch(0.55_0.02_255/0.06),0_18px_40px_-28px_oklch(0.45_0.05_255/0.45)] divide-y divide-ttg01-edge overflow-hidden">
<label for="ttg-01-sw" class="flex items-center gap-6 min-h-16 px-5 py-4 cursor-pointer transition-colors duration-150 hover:bg-ttg01-tint">
<span class="flex flex-col gap-0.5 flex-1">
<span class="text-[15.5px] font-semibold text-ttg01-ink">Wi-Fi</span>
<span class="text-[12.5px] text-ttg01-mute">Auto-join known networks</span>
</span>
<input type="checkbox" id="ttg-01-sw" role="switch" checked class="appearance-none shrink-0 w-[52px] h-8 rounded-full bg-ttg01-off relative cursor-pointer transition-colors duration-300 shadow-[inset_0_0_0_1px_oklch(0.55_0.02_255/0.08)] before:content-[''] before:absolute before:top-0.5 before:left-0.5 before:w-7 before:h-7 before:rounded-full before:bg-white before:shadow-[0_3px_8px_oklch(0.3_0.02_255/0.18),0_1px_1px_oklch(0.3_0.02_255/0.16)] before:transition-[transform,width] before:duration-300 before:ease-[cubic-bezier(0.2,0.9,0.25,1.05)] checked:bg-ttg01-on checked:before:translate-x-5 active:before:w-8 checked:active:before:translate-x-[15px] focus-visible:outline-3 focus-visible:outline-ttg01-focus focus-visible:outline-offset-3 motion-reduce:transition-none motion-reduce:before:transition-none">
</label>
<label for="ttg-01-bt" class="flex items-center gap-6 min-h-16 px-5 py-4 cursor-pointer transition-colors duration-150 hover:bg-ttg01-tint">
<span class="flex flex-col gap-0.5 flex-1">
<span class="text-[15.5px] font-semibold text-ttg01-ink">Bluetooth</span>
<span class="text-[12.5px] text-ttg01-mute">Discoverable as “Studio”</span>
</span>
<input type="checkbox" id="ttg-01-bt" role="switch" class="appearance-none shrink-0 w-[52px] h-8 rounded-full bg-ttg01-off relative cursor-pointer transition-colors duration-300 shadow-[inset_0_0_0_1px_oklch(0.55_0.02_255/0.08)] before:content-[''] before:absolute before:top-0.5 before:left-0.5 before:w-7 before:h-7 before:rounded-full before:bg-white before:shadow-[0_3px_8px_oklch(0.3_0.02_255/0.18),0_1px_1px_oklch(0.3_0.02_255/0.16)] before:transition-[transform,width] before:duration-300 before:ease-[cubic-bezier(0.2,0.9,0.25,1.05)] checked:bg-ttg01-on checked:before:translate-x-5 active:before:w-8 checked:active:before:translate-x-[15px] focus-visible:outline-3 focus-visible:outline-ttg01-focus focus-visible:outline-offset-3 motion-reduce:transition-none motion-reduce:before:transition-none">
</label>
</div>
<p class="font-ttg01-mono text-[11px] tracking-[0.14em] uppercase text-ttg01-mute">Click · or Tab + Space</p>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-ttg01-bg: #f1f2f6;
--color-ttg01-bg: oklch(0.96 0.005 255);
--color-ttg01-edge: #e6e7ee;
--color-ttg01-edge: oklch(0.93 0.005 255);
--color-ttg01-tint: #f7f8fb;
--color-ttg01-tint: oklch(0.98 0.004 255);
--color-ttg01-ink: #1c1d22;
--color-ttg01-ink: oklch(0.24 0.008 265);
--color-ttg01-mute: #8b8e99;
--color-ttg01-mute: oklch(0.63 0.012 265);
--color-ttg01-off: #e4e4ea;
--color-ttg01-off: oklch(0.92 0.004 265);
--color-ttg01-on: #2fbf5b;
--color-ttg01-on: oklch(0.72 0.18 149);
--color-ttg01-focus: #0a84ff;
--color-ttg01-focus: oklch(0.62 0.2 256);
--font-ttg01-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--font-ttg01-mono: 'JetBrains Mono', ui-monospace, Menlo, monospace;
}
/* Root rule: the full-bleed contract, also stated in the markup's utilities, so the
stage is sized correctly before the browser build has compiled. */
.ttg-01 {
width: 100%;
min-height: 100svh;
}
/* Older Safari still wants the prefix on a checkbox. */
.ttg-01 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 iOS-Style Toggle Switch with appearance-none
Source: https://codefronts.com/components/tailwind-css-toggles-switches/tailwind-ios-style-toggle-switch-appearance-none/
The most searched toggle on earth, ported: appearance-none turns the checkbox itself into the 52×32 track, a before: thumb glides across on checked:before:translate-x-5, and active:before:w-8 reproduces the native iOS squash. The port's win is that the resting, on, pressed and reduced-motion states all sit in one class list on the input.
## HTML
```html
<section class="ttg-01 w-full min-h-svh flex flex-col items-center justify-center gap-6 px-5 py-14 bg-ttg01-bg font-ttg01-sans" aria-label="iOS-style toggle switch">
<div class="w-[min(360px,100%)] rounded-3xl bg-white border border-ttg01-edge shadow-[0_1px_2px_oklch(0.55_0.02_255/0.06),0_18px_40px_-28px_oklch(0.45_0.05_255/0.45)] divide-y divide-ttg01-edge overflow-hidden">
<label for="ttg-01-sw" class="flex items-center gap-6 min-h-16 px-5 py-4 cursor-pointer transition-colors duration-150 hover:bg-ttg01-tint">
<span class="flex flex-col gap-0.5 flex-1">
<span class="text-[15.5px] font-semibold text-ttg01-ink">Wi-Fi</span>
<span class="text-[12.5px] text-ttg01-mute">Auto-join known networks</span>
</span>
<input type="checkbox" id="ttg-01-sw" role="switch" checked class="appearance-none shrink-0 w-[52px] h-8 rounded-full bg-ttg01-off relative cursor-pointer transition-colors duration-300 shadow-[inset_0_0_0_1px_oklch(0.55_0.02_255/0.08)] before:content-[''] before:absolute before:top-0.5 before:left-0.5 before:w-7 before:h-7 before:rounded-full before:bg-white before:shadow-[0_3px_8px_oklch(0.3_0.02_255/0.18),0_1px_1px_oklch(0.3_0.02_255/0.16)] before:transition-[transform,width] before:duration-300 before:ease-[cubic-bezier(0.2,0.9,0.25,1.05)] checked:bg-ttg01-on checked:before:translate-x-5 active:before:w-8 checked:active:before:translate-x-[15px] focus-visible:outline-3 focus-visible:outline-ttg01-focus focus-visible:outline-offset-3 motion-reduce:transition-none motion-reduce:before:transition-none">
</label>
<label for="ttg-01-bt" class="flex items-center gap-6 min-h-16 px-5 py-4 cursor-pointer transition-colors duration-150 hover:bg-ttg01-tint">
<span class="flex flex-col gap-0.5 flex-1">
<span class="text-[15.5px] font-semibold text-ttg01-ink">Bluetooth</span>
<span class="text-[12.5px] text-ttg01-mute">Discoverable as “Studio”</span>
</span>
<input type="checkbox" id="ttg-01-bt" role="switch" class="appearance-none shrink-0 w-[52px] h-8 rounded-full bg-ttg01-off relative cursor-pointer transition-colors duration-300 shadow-[inset_0_0_0_1px_oklch(0.55_0.02_255/0.08)] before:content-[''] before:absolute before:top-0.5 before:left-0.5 before:w-7 before:h-7 before:rounded-full before:bg-white before:shadow-[0_3px_8px_oklch(0.3_0.02_255/0.18),0_1px_1px_oklch(0.3_0.02_255/0.16)] before:transition-[transform,width] before:duration-300 before:ease-[cubic-bezier(0.2,0.9,0.25,1.05)] checked:bg-ttg01-on checked:before:translate-x-5 active:before:w-8 checked:active:before:translate-x-[15px] focus-visible:outline-3 focus-visible:outline-ttg01-focus focus-visible:outline-offset-3 motion-reduce:transition-none motion-reduce:before:transition-none">
</label>
</div>
<p class="font-ttg01-mono text-[11px] tracking-[0.14em] uppercase text-ttg01-mute">Click · or Tab + Space</p>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-ttg01-bg: #f1f2f6;
--color-ttg01-bg: oklch(0.96 0.005 255);
--color-ttg01-edge: #e6e7ee;
--color-ttg01-edge: oklch(0.93 0.005 255);
--color-ttg01-tint: #f7f8fb;
--color-ttg01-tint: oklch(0.98 0.004 255);
--color-ttg01-ink: #1c1d22;
--color-ttg01-ink: oklch(0.24 0.008 265);
--color-ttg01-mute: #8b8e99;
--color-ttg01-mute: oklch(0.63 0.012 265);
--color-ttg01-off: #e4e4ea;
--color-ttg01-off: oklch(0.92 0.004 265);
--color-ttg01-on: #2fbf5b;
--color-ttg01-on: oklch(0.72 0.18 149);
--color-ttg01-focus: #0a84ff;
--color-ttg01-focus: oklch(0.62 0.2 256);
--font-ttg01-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--font-ttg01-mono: 'JetBrains Mono', ui-monospace, Menlo, monospace;
}
/* Root rule: the full-bleed contract, also stated in the markup's utilities, so the
stage is sized correctly before the browser build has compiled. */
.ttg-01 {
width: 100%;
min-height: 100svh;
}
/* Older Safari still wants the prefix on a checkbox. */
.ttg-01 input[type="checkbox"] {
-webkit-appearance: none;
}
```How this works
The input is the switch. appearance-none strips the native checkbox rendering, and everything after it treats the input as an ordinary box: w-[52px] h-8 rounded-full relative. Space, Tab, form submission and role="switch" announcement survive untouched, because nothing was replaced — only repainted.
The thumb travels on transform. before:absolute before:top-0.5 before:left-0.5 before:size-7 parks it in the socket; checked:before:translate-x-5 moves it 20px, which is exactly width − height + 2 × inset. Keeping that arithmetic beside the utilities that produce it is the reason a resized track stays correct.
Two durations, deliberately. The track colour fades on duration-300 with the default ease; the thumb rides ease-[cubic-bezier(0.2,0.9,0.25,1.05)], a curve with the faintest overshoot. Colour that overshoots reads as flicker; position that overshoots reads as physical.
Make it yours
- Brand it through one token:
--color-ttg01-ondrives the on track, the focus ring tint and the label accent. - Resize by editing the width/height pair, then set the travel to
width − height + 4px— e.g. a 44×26 track travels 16px (checked:before:translate-x-4). - Drop
active:before:w-8for a stricter, less playful control. - Swap
rounded-fullforrounded-lgon track and thumb to get a squircle switch.
Gotchas — read before shipping
- Never
hiddenordisplay:nonea styled checkbox — it leaves the tab order.appearance-nonekeeps the element focusable, which is the entire reason this pattern beats a div. - Tailwind's
checked:variant compiles to&:checked, so it only works on the input itself; to style a sibling from state, the element needspeer/peer-checked:instead. active:before:w-8animateswidth. On a 31px element that is free, but never move the glide itself offtransform.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
appearance:none on checkboxes and role="switch" are universal in modern browsers; the floor quoted is Tailwind v4's own generated sheet (cascade layers, @property, color-mix()).