33 / 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 Master Toggle with Indeterminate Tri-State
What demo 13 could not do: a master switch that actually SETS the group, with a real third state. When some children are on, the master goes indeterminate — a dash, not a thumb — using the native pseudo-class and aria-checked="mixed".
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="ttg-33 w-full min-h-svh grid place-items-center px-5 py-14 bg-ttg33-bg font-ttg33-sans" aria-label="Master toggle with indeterminate state">
<div id="ttg-33-group" class="w-[min(390px,100%)] rounded-[20px] bg-white overflow-hidden border border-ttg33-hair shadow-[0_24px_50px_-34px_oklch(0.4_0.04_265/0.6)]">
<label class="flex items-center gap-5 min-h-16 px-5 py-4 cursor-pointer bg-ttg33-headbg border-b border-ttg33-hair">
<span class="flex-1 flex flex-col gap-0.5">
<b class="text-[15.5px] font-bold text-ttg33-ink">All notifications</b>
<small id="ttg-33-summary" class="text-[12px] text-ttg33-mute">2 of 3 enabled</small>
</span>
<input type="checkbox" id="ttg-33-master" role="switch" aria-checked="mixed" aria-label="All notifications" class="appearance-none shrink-0 w-13 h-7.5 rounded-full bg-ttg33-trackoff relative cursor-pointer transition-colors duration-250 before:content-[''] before:absolute before:top-0.5 before:left-0.5 before:w-6.5 before:h-6.5 before:rounded-full before:bg-white before:shadow-[0_2px_5px_oklch(0.3_0.03_265/0.35)] before:transition-transform before:duration-250 before:ease-[cubic-bezier(0.25,0.9,0.3,1.1)] after:content-[''] after:absolute after:top-1/2 after:left-1/2 after:-mt-[1.5px] after:-ml-[7px] after:w-3.5 after:h-[3px] after:rounded-full after:bg-white after:opacity-0 after:transition-opacity after:duration-200 checked:bg-ttg33-on checked:before:translate-x-[22px] indeterminate:bg-ttg33-mixed indeterminate:before:translate-x-[11px] indeterminate:after:opacity-100 focus-visible:outline-3 focus-visible:outline-ttg33-focus focus-visible:outline-offset-2 motion-reduce:transition-none motion-reduce:before:transition-none">
</label>
<label class="flex items-center gap-5 min-h-15 px-5 py-3.5 cursor-pointer transition-colors duration-200 hover:bg-ttg33-tint has-checked:[&_.ttg33-t]:text-ttg33-ink">
<span class="flex-1 flex flex-col gap-0.5">
<b class="ttg33-t text-[14.5px] font-semibold text-ttg33-dim transition-colors duration-200">Direct messages</b>
<small class="text-[12px] text-ttg33-mute">Push and email</small>
</span>
<input type="checkbox" role="switch" checked data-child aria-label="Direct messages" class="appearance-none shrink-0 w-11 h-6.5 rounded-full bg-ttg33-trackoff relative cursor-pointer transition-colors duration-250 before:content-[''] before:absolute before:top-0.5 before:left-0.5 before:w-[22px] before:h-[22px] before:rounded-full before:bg-white before:shadow-[0_2px_5px_oklch(0.3_0.03_265/0.3)] before:transition-transform before:duration-250 before:ease-[cubic-bezier(0.25,0.9,0.3,1.1)] checked:bg-ttg33-on checked:before:translate-x-[18px] focus-visible:outline-3 focus-visible:outline-ttg33-focus focus-visible:outline-offset-2 motion-reduce:transition-none motion-reduce:before:transition-none">
</label>
<label class="flex items-center gap-5 min-h-15 px-5 py-3.5 cursor-pointer border-t border-ttg33-hair transition-colors duration-200 hover:bg-ttg33-tint has-checked:[&_.ttg33-t]:text-ttg33-ink">
<span class="flex-1 flex flex-col gap-0.5">
<b class="ttg33-t text-[14.5px] font-semibold text-ttg33-dim transition-colors duration-200">Mentions</b>
<small class="text-[12px] text-ttg33-mute">Only when someone @s you</small>
</span>
<input type="checkbox" role="switch" checked data-child aria-label="Mentions" class="appearance-none shrink-0 w-11 h-6.5 rounded-full bg-ttg33-trackoff relative cursor-pointer transition-colors duration-250 before:content-[''] before:absolute before:top-0.5 before:left-0.5 before:w-[22px] before:h-[22px] before:rounded-full before:bg-white before:shadow-[0_2px_5px_oklch(0.3_0.03_265/0.3)] before:transition-transform before:duration-250 before:ease-[cubic-bezier(0.25,0.9,0.3,1.1)] checked:bg-ttg33-on checked:before:translate-x-[18px] focus-visible:outline-3 focus-visible:outline-ttg33-focus focus-visible:outline-offset-2 motion-reduce:transition-none motion-reduce:before:transition-none">
</label>
<label class="flex items-center gap-5 min-h-15 px-5 py-3.5 cursor-pointer border-t border-ttg33-hair transition-colors duration-200 hover:bg-ttg33-tint has-checked:[&_.ttg33-t]:text-ttg33-ink">
<span class="flex-1 flex flex-col gap-0.5">
<b class="ttg33-t text-[14.5px] font-semibold text-ttg33-dim transition-colors duration-200">Product news</b>
<small class="text-[12px] text-ttg33-mute">Monthly, at most</small>
</span>
<input type="checkbox" role="switch" data-child aria-label="Product news" class="appearance-none shrink-0 w-11 h-6.5 rounded-full bg-ttg33-trackoff relative cursor-pointer transition-colors duration-250 before:content-[''] before:absolute before:top-0.5 before:left-0.5 before:w-[22px] before:h-[22px] before:rounded-full before:bg-white before:shadow-[0_2px_5px_oklch(0.3_0.03_265/0.3)] before:transition-transform before:duration-250 before:ease-[cubic-bezier(0.25,0.9,0.3,1.1)] checked:bg-ttg33-on checked:before:translate-x-[18px] focus-visible:outline-3 focus-visible:outline-ttg33-focus focus-visible:outline-offset-2 motion-reduce:transition-none motion-reduce:before:transition-none">
</label>
</div>
</section><section class="ttg-33 w-full min-h-svh grid place-items-center px-5 py-14 bg-ttg33-bg font-ttg33-sans" aria-label="Master toggle with indeterminate state">
<div id="ttg-33-group" class="w-[min(390px,100%)] rounded-[20px] bg-white overflow-hidden border border-ttg33-hair shadow-[0_24px_50px_-34px_oklch(0.4_0.04_265/0.6)]">
<label class="flex items-center gap-5 min-h-16 px-5 py-4 cursor-pointer bg-ttg33-headbg border-b border-ttg33-hair">
<span class="flex-1 flex flex-col gap-0.5">
<b class="text-[15.5px] font-bold text-ttg33-ink">All notifications</b>
<small id="ttg-33-summary" class="text-[12px] text-ttg33-mute">2 of 3 enabled</small>
</span>
<input type="checkbox" id="ttg-33-master" role="switch" aria-checked="mixed" aria-label="All notifications" class="appearance-none shrink-0 w-13 h-7.5 rounded-full bg-ttg33-trackoff relative cursor-pointer transition-colors duration-250 before:content-[''] before:absolute before:top-0.5 before:left-0.5 before:w-6.5 before:h-6.5 before:rounded-full before:bg-white before:shadow-[0_2px_5px_oklch(0.3_0.03_265/0.35)] before:transition-transform before:duration-250 before:ease-[cubic-bezier(0.25,0.9,0.3,1.1)] after:content-[''] after:absolute after:top-1/2 after:left-1/2 after:-mt-[1.5px] after:-ml-[7px] after:w-3.5 after:h-[3px] after:rounded-full after:bg-white after:opacity-0 after:transition-opacity after:duration-200 checked:bg-ttg33-on checked:before:translate-x-[22px] indeterminate:bg-ttg33-mixed indeterminate:before:translate-x-[11px] indeterminate:after:opacity-100 focus-visible:outline-3 focus-visible:outline-ttg33-focus focus-visible:outline-offset-2 motion-reduce:transition-none motion-reduce:before:transition-none">
</label>
<label class="flex items-center gap-5 min-h-15 px-5 py-3.5 cursor-pointer transition-colors duration-200 hover:bg-ttg33-tint has-checked:[&_.ttg33-t]:text-ttg33-ink">
<span class="flex-1 flex flex-col gap-0.5">
<b class="ttg33-t text-[14.5px] font-semibold text-ttg33-dim transition-colors duration-200">Direct messages</b>
<small class="text-[12px] text-ttg33-mute">Push and email</small>
</span>
<input type="checkbox" role="switch" checked data-child aria-label="Direct messages" class="appearance-none shrink-0 w-11 h-6.5 rounded-full bg-ttg33-trackoff relative cursor-pointer transition-colors duration-250 before:content-[''] before:absolute before:top-0.5 before:left-0.5 before:w-[22px] before:h-[22px] before:rounded-full before:bg-white before:shadow-[0_2px_5px_oklch(0.3_0.03_265/0.3)] before:transition-transform before:duration-250 before:ease-[cubic-bezier(0.25,0.9,0.3,1.1)] checked:bg-ttg33-on checked:before:translate-x-[18px] focus-visible:outline-3 focus-visible:outline-ttg33-focus focus-visible:outline-offset-2 motion-reduce:transition-none motion-reduce:before:transition-none">
</label>
<label class="flex items-center gap-5 min-h-15 px-5 py-3.5 cursor-pointer border-t border-ttg33-hair transition-colors duration-200 hover:bg-ttg33-tint has-checked:[&_.ttg33-t]:text-ttg33-ink">
<span class="flex-1 flex flex-col gap-0.5">
<b class="ttg33-t text-[14.5px] font-semibold text-ttg33-dim transition-colors duration-200">Mentions</b>
<small class="text-[12px] text-ttg33-mute">Only when someone @s you</small>
</span>
<input type="checkbox" role="switch" checked data-child aria-label="Mentions" class="appearance-none shrink-0 w-11 h-6.5 rounded-full bg-ttg33-trackoff relative cursor-pointer transition-colors duration-250 before:content-[''] before:absolute before:top-0.5 before:left-0.5 before:w-[22px] before:h-[22px] before:rounded-full before:bg-white before:shadow-[0_2px_5px_oklch(0.3_0.03_265/0.3)] before:transition-transform before:duration-250 before:ease-[cubic-bezier(0.25,0.9,0.3,1.1)] checked:bg-ttg33-on checked:before:translate-x-[18px] focus-visible:outline-3 focus-visible:outline-ttg33-focus focus-visible:outline-offset-2 motion-reduce:transition-none motion-reduce:before:transition-none">
</label>
<label class="flex items-center gap-5 min-h-15 px-5 py-3.5 cursor-pointer border-t border-ttg33-hair transition-colors duration-200 hover:bg-ttg33-tint has-checked:[&_.ttg33-t]:text-ttg33-ink">
<span class="flex-1 flex flex-col gap-0.5">
<b class="ttg33-t text-[14.5px] font-semibold text-ttg33-dim transition-colors duration-200">Product news</b>
<small class="text-[12px] text-ttg33-mute">Monthly, at most</small>
</span>
<input type="checkbox" role="switch" data-child aria-label="Product news" class="appearance-none shrink-0 w-11 h-6.5 rounded-full bg-ttg33-trackoff relative cursor-pointer transition-colors duration-250 before:content-[''] before:absolute before:top-0.5 before:left-0.5 before:w-[22px] before:h-[22px] before:rounded-full before:bg-white before:shadow-[0_2px_5px_oklch(0.3_0.03_265/0.3)] before:transition-transform before:duration-250 before:ease-[cubic-bezier(0.25,0.9,0.3,1.1)] checked:bg-ttg33-on checked:before:translate-x-[18px] focus-visible:outline-3 focus-visible:outline-ttg33-focus focus-visible:outline-offset-2 motion-reduce:transition-none motion-reduce:before:transition-none">
</label>
</div>
</section>@import "tailwindcss";
@theme {
--color-ttg33-bg: #edeef4;
--color-ttg33-bg: oklch(0.95 0.006 265);
--color-ttg33-headbg: #f7f8fc;
--color-ttg33-headbg: oklch(0.98 0.004 265);
--color-ttg33-hair: #e8eaf1;
--color-ttg33-hair: oklch(0.93 0.006 265);
--color-ttg33-tint: #f8f9fc;
--color-ttg33-tint: oklch(0.98 0.004 265);
--color-ttg33-ink: #1f2532;
--color-ttg33-ink: oklch(0.26 0.015 265);
--color-ttg33-dim: #737b8d;
--color-ttg33-dim: oklch(0.58 0.018 265);
--color-ttg33-mute: #8f97a7;
--color-ttg33-mute: oklch(0.65 0.015 265);
--color-ttg33-trackoff: #dfe2ea;
--color-ttg33-trackoff: oklch(0.9 0.008 265);
--color-ttg33-on: #2f9e63;
--color-ttg33-on: oklch(0.63 0.16 155);
--color-ttg33-mixed: #4f74e8;
--color-ttg33-mixed: oklch(0.58 0.17 265);
--color-ttg33-focus: #3f63e0;
--color-ttg33-focus: oklch(0.55 0.19 265);
--font-ttg33-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
.ttg-33 {
width: 100%;
min-height: 100svh;
}
.ttg-33 input[type="checkbox"] {
-webkit-appearance: none;
}
/* The mixed dash sits on the thumb, so it needs the thumb's colour, not the track's. */
.ttg-33 #ttg-33-master:indeterminate::after {
opacity: 1;
background-color: var(--color-ttg33-mixed);
}@import "tailwindcss";
@theme {
--color-ttg33-bg: #edeef4;
--color-ttg33-bg: oklch(0.95 0.006 265);
--color-ttg33-headbg: #f7f8fc;
--color-ttg33-headbg: oklch(0.98 0.004 265);
--color-ttg33-hair: #e8eaf1;
--color-ttg33-hair: oklch(0.93 0.006 265);
--color-ttg33-tint: #f8f9fc;
--color-ttg33-tint: oklch(0.98 0.004 265);
--color-ttg33-ink: #1f2532;
--color-ttg33-ink: oklch(0.26 0.015 265);
--color-ttg33-dim: #737b8d;
--color-ttg33-dim: oklch(0.58 0.018 265);
--color-ttg33-mute: #8f97a7;
--color-ttg33-mute: oklch(0.65 0.015 265);
--color-ttg33-trackoff: #dfe2ea;
--color-ttg33-trackoff: oklch(0.9 0.008 265);
--color-ttg33-on: #2f9e63;
--color-ttg33-on: oklch(0.63 0.16 155);
--color-ttg33-mixed: #4f74e8;
--color-ttg33-mixed: oklch(0.58 0.17 265);
--color-ttg33-focus: #3f63e0;
--color-ttg33-focus: oklch(0.55 0.19 265);
--font-ttg33-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
.ttg-33 {
width: 100%;
min-height: 100svh;
}
.ttg-33 input[type="checkbox"] {
-webkit-appearance: none;
}
/* The mixed dash sits on the thumb, so it needs the thumb's colour, not the track's. */
.ttg-33 #ttg-33-master:indeterminate::after {
opacity: 1;
background-color: var(--color-ttg33-mixed);
}(() => {
const group = document.querySelector('#ttg-33-group');
const master = document.querySelector('#ttg-33-master');
const summary = document.querySelector('#ttg-33-summary');
if (!group || !master || !summary) return;
const kids = () => [...group.querySelectorAll('input[data-child]')];
const sync = () => {
const all = kids();
const on = all.filter((k) => k.checked).length;
master.checked = on === all.length;
master.indeterminate = on > 0 && on < all.length;
// :indeterminate is invisible to assistive tech — mirror it.
master.setAttribute('aria-checked', master.indeterminate ? 'mixed' : String(master.checked));
summary.textContent = on + ' of ' + all.length + ' enabled';
};
master.addEventListener('change', () => {
kids().forEach((k) => { k.checked = master.checked; });
sync();
});
group.addEventListener('change', (e) => { if (e.target.matches('input[data-child]')) sync(); });
sync();
})();(() => {
const group = document.querySelector('#ttg-33-group');
const master = document.querySelector('#ttg-33-master');
const summary = document.querySelector('#ttg-33-summary');
if (!group || !master || !summary) return;
const kids = () => [...group.querySelectorAll('input[data-child]')];
const sync = () => {
const all = kids();
const on = all.filter((k) => k.checked).length;
master.checked = on === all.length;
master.indeterminate = on > 0 && on < all.length;
// :indeterminate is invisible to assistive tech — mirror it.
master.setAttribute('aria-checked', master.indeterminate ? 'mixed' : String(master.checked));
summary.textContent = on + ' of ' + all.length + ' enabled';
};
master.addEventListener('change', () => {
kids().forEach((k) => { k.checked = master.checked; });
sync();
});
group.addEventListener('change', (e) => { if (e.target.matches('input[data-child]')) sync(); });
sync();
})();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-ttg33-bg: #edeef4;
--color-ttg33-bg: oklch(0.95 0.006 265);
--color-ttg33-headbg: #f7f8fc;
--color-ttg33-headbg: oklch(0.98 0.004 265);
--color-ttg33-hair: #e8eaf1;
--color-ttg33-hair: oklch(0.93 0.006 265);
--color-ttg33-tint: #f8f9fc;
--color-ttg33-tint: oklch(0.98 0.004 265);
--color-ttg33-ink: #1f2532;
--color-ttg33-ink: oklch(0.26 0.015 265);
--color-ttg33-dim: #737b8d;
--color-ttg33-dim: oklch(0.58 0.018 265);
--color-ttg33-mute: #8f97a7;
--color-ttg33-mute: oklch(0.65 0.015 265);
--color-ttg33-trackoff: #dfe2ea;
--color-ttg33-trackoff: oklch(0.9 0.008 265);
--color-ttg33-on: #2f9e63;
--color-ttg33-on: oklch(0.63 0.16 155);
--color-ttg33-mixed: #4f74e8;
--color-ttg33-mixed: oklch(0.58 0.17 265);
--color-ttg33-focus: #3f63e0;
--color-ttg33-focus: oklch(0.55 0.19 265);
--font-ttg33-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
.ttg-33 {
width: 100%;
min-height: 100svh;
}
.ttg-33 input[type="checkbox"] {
-webkit-appearance: none;
}
/* The mixed dash sits on the thumb, so it needs the thumb's colour, not the track's. */
.ttg-33 #ttg-33-master:indeterminate::after {
opacity: 1;
background-color: var(--color-ttg33-mixed);
}@import "tailwindcss";
@theme {
--color-ttg33-bg: #edeef4;
--color-ttg33-bg: oklch(0.95 0.006 265);
--color-ttg33-headbg: #f7f8fc;
--color-ttg33-headbg: oklch(0.98 0.004 265);
--color-ttg33-hair: #e8eaf1;
--color-ttg33-hair: oklch(0.93 0.006 265);
--color-ttg33-tint: #f8f9fc;
--color-ttg33-tint: oklch(0.98 0.004 265);
--color-ttg33-ink: #1f2532;
--color-ttg33-ink: oklch(0.26 0.015 265);
--color-ttg33-dim: #737b8d;
--color-ttg33-dim: oklch(0.58 0.018 265);
--color-ttg33-mute: #8f97a7;
--color-ttg33-mute: oklch(0.65 0.015 265);
--color-ttg33-trackoff: #dfe2ea;
--color-ttg33-trackoff: oklch(0.9 0.008 265);
--color-ttg33-on: #2f9e63;
--color-ttg33-on: oklch(0.63 0.16 155);
--color-ttg33-mixed: #4f74e8;
--color-ttg33-mixed: oklch(0.58 0.17 265);
--color-ttg33-focus: #3f63e0;
--color-ttg33-focus: oklch(0.55 0.19 265);
--font-ttg33-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
.ttg-33 {
width: 100%;
min-height: 100svh;
}
.ttg-33 input[type="checkbox"] {
-webkit-appearance: none;
}
/* The mixed dash sits on the thumb, so it needs the thumb's colour, not the track's. */
.ttg-33 #ttg-33-master:indeterminate::after {
opacity: 1;
background-color: var(--color-ttg33-mixed);
}<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;800&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;800&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
indeterminate:is a Tailwind variant for a state that has no HTML attribute at all — the clearest example of the framework exposing a DOM property to the class list, and the reason this control needs no state classes.- Three states get three SHAPES: thumb left, thumb centre with a dash, thumb right. Colour alone across three states is unreadable for a significant share of users, and the middle position is what makes 'some' feel like a position rather than an error.
- The dash colour lives in one scoped rule because the pseudo-element needs the mixed tone while the thumb stays white — a variant could express it, but not more clearly.
- UI upgrade over demo 13: the master now SETS the group instead of only reporting on it, the summary is a live count rather than a three-way badge, and
aria-checked="mixed"means screen-reader users get the tri-state that sighted users can see.
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 Master Toggle with Indeterminate Tri-State
Source: https://codefronts.com/components/tailwind-css-toggles-switches/tailwind-master-toggle-indeterminate-tri-state/
What demo 13 could not do: a master switch that actually SETS the group, with a real third state. When some children are on, the master goes indeterminate — a dash, not a thumb — using the native pseudo-class and aria-checked="mixed".
## HTML
```html
<section class="ttg-33 w-full min-h-svh grid place-items-center px-5 py-14 bg-ttg33-bg font-ttg33-sans" aria-label="Master toggle with indeterminate state">
<div id="ttg-33-group" class="w-[min(390px,100%)] rounded-[20px] bg-white overflow-hidden border border-ttg33-hair shadow-[0_24px_50px_-34px_oklch(0.4_0.04_265/0.6)]">
<label class="flex items-center gap-5 min-h-16 px-5 py-4 cursor-pointer bg-ttg33-headbg border-b border-ttg33-hair">
<span class="flex-1 flex flex-col gap-0.5">
<b class="text-[15.5px] font-bold text-ttg33-ink">All notifications</b>
<small id="ttg-33-summary" class="text-[12px] text-ttg33-mute">2 of 3 enabled</small>
</span>
<input type="checkbox" id="ttg-33-master" role="switch" aria-checked="mixed" aria-label="All notifications" class="appearance-none shrink-0 w-13 h-7.5 rounded-full bg-ttg33-trackoff relative cursor-pointer transition-colors duration-250 before:content-[''] before:absolute before:top-0.5 before:left-0.5 before:w-6.5 before:h-6.5 before:rounded-full before:bg-white before:shadow-[0_2px_5px_oklch(0.3_0.03_265/0.35)] before:transition-transform before:duration-250 before:ease-[cubic-bezier(0.25,0.9,0.3,1.1)] after:content-[''] after:absolute after:top-1/2 after:left-1/2 after:-mt-[1.5px] after:-ml-[7px] after:w-3.5 after:h-[3px] after:rounded-full after:bg-white after:opacity-0 after:transition-opacity after:duration-200 checked:bg-ttg33-on checked:before:translate-x-[22px] indeterminate:bg-ttg33-mixed indeterminate:before:translate-x-[11px] indeterminate:after:opacity-100 focus-visible:outline-3 focus-visible:outline-ttg33-focus focus-visible:outline-offset-2 motion-reduce:transition-none motion-reduce:before:transition-none">
</label>
<label class="flex items-center gap-5 min-h-15 px-5 py-3.5 cursor-pointer transition-colors duration-200 hover:bg-ttg33-tint has-checked:[&_.ttg33-t]:text-ttg33-ink">
<span class="flex-1 flex flex-col gap-0.5">
<b class="ttg33-t text-[14.5px] font-semibold text-ttg33-dim transition-colors duration-200">Direct messages</b>
<small class="text-[12px] text-ttg33-mute">Push and email</small>
</span>
<input type="checkbox" role="switch" checked data-child aria-label="Direct messages" class="appearance-none shrink-0 w-11 h-6.5 rounded-full bg-ttg33-trackoff relative cursor-pointer transition-colors duration-250 before:content-[''] before:absolute before:top-0.5 before:left-0.5 before:w-[22px] before:h-[22px] before:rounded-full before:bg-white before:shadow-[0_2px_5px_oklch(0.3_0.03_265/0.3)] before:transition-transform before:duration-250 before:ease-[cubic-bezier(0.25,0.9,0.3,1.1)] checked:bg-ttg33-on checked:before:translate-x-[18px] focus-visible:outline-3 focus-visible:outline-ttg33-focus focus-visible:outline-offset-2 motion-reduce:transition-none motion-reduce:before:transition-none">
</label>
<label class="flex items-center gap-5 min-h-15 px-5 py-3.5 cursor-pointer border-t border-ttg33-hair transition-colors duration-200 hover:bg-ttg33-tint has-checked:[&_.ttg33-t]:text-ttg33-ink">
<span class="flex-1 flex flex-col gap-0.5">
<b class="ttg33-t text-[14.5px] font-semibold text-ttg33-dim transition-colors duration-200">Mentions</b>
<small class="text-[12px] text-ttg33-mute">Only when someone @s you</small>
</span>
<input type="checkbox" role="switch" checked data-child aria-label="Mentions" class="appearance-none shrink-0 w-11 h-6.5 rounded-full bg-ttg33-trackoff relative cursor-pointer transition-colors duration-250 before:content-[''] before:absolute before:top-0.5 before:left-0.5 before:w-[22px] before:h-[22px] before:rounded-full before:bg-white before:shadow-[0_2px_5px_oklch(0.3_0.03_265/0.3)] before:transition-transform before:duration-250 before:ease-[cubic-bezier(0.25,0.9,0.3,1.1)] checked:bg-ttg33-on checked:before:translate-x-[18px] focus-visible:outline-3 focus-visible:outline-ttg33-focus focus-visible:outline-offset-2 motion-reduce:transition-none motion-reduce:before:transition-none">
</label>
<label class="flex items-center gap-5 min-h-15 px-5 py-3.5 cursor-pointer border-t border-ttg33-hair transition-colors duration-200 hover:bg-ttg33-tint has-checked:[&_.ttg33-t]:text-ttg33-ink">
<span class="flex-1 flex flex-col gap-0.5">
<b class="ttg33-t text-[14.5px] font-semibold text-ttg33-dim transition-colors duration-200">Product news</b>
<small class="text-[12px] text-ttg33-mute">Monthly, at most</small>
</span>
<input type="checkbox" role="switch" data-child aria-label="Product news" class="appearance-none shrink-0 w-11 h-6.5 rounded-full bg-ttg33-trackoff relative cursor-pointer transition-colors duration-250 before:content-[''] before:absolute before:top-0.5 before:left-0.5 before:w-[22px] before:h-[22px] before:rounded-full before:bg-white before:shadow-[0_2px_5px_oklch(0.3_0.03_265/0.3)] before:transition-transform before:duration-250 before:ease-[cubic-bezier(0.25,0.9,0.3,1.1)] checked:bg-ttg33-on checked:before:translate-x-[18px] focus-visible:outline-3 focus-visible:outline-ttg33-focus focus-visible:outline-offset-2 motion-reduce:transition-none motion-reduce:before:transition-none">
</label>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-ttg33-bg: #edeef4;
--color-ttg33-bg: oklch(0.95 0.006 265);
--color-ttg33-headbg: #f7f8fc;
--color-ttg33-headbg: oklch(0.98 0.004 265);
--color-ttg33-hair: #e8eaf1;
--color-ttg33-hair: oklch(0.93 0.006 265);
--color-ttg33-tint: #f8f9fc;
--color-ttg33-tint: oklch(0.98 0.004 265);
--color-ttg33-ink: #1f2532;
--color-ttg33-ink: oklch(0.26 0.015 265);
--color-ttg33-dim: #737b8d;
--color-ttg33-dim: oklch(0.58 0.018 265);
--color-ttg33-mute: #8f97a7;
--color-ttg33-mute: oklch(0.65 0.015 265);
--color-ttg33-trackoff: #dfe2ea;
--color-ttg33-trackoff: oklch(0.9 0.008 265);
--color-ttg33-on: #2f9e63;
--color-ttg33-on: oklch(0.63 0.16 155);
--color-ttg33-mixed: #4f74e8;
--color-ttg33-mixed: oklch(0.58 0.17 265);
--color-ttg33-focus: #3f63e0;
--color-ttg33-focus: oklch(0.55 0.19 265);
--font-ttg33-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
.ttg-33 {
width: 100%;
min-height: 100svh;
}
.ttg-33 input[type="checkbox"] {
-webkit-appearance: none;
}
/* The mixed dash sits on the thumb, so it needs the thumb's colour, not the track's. */
.ttg-33 #ttg-33-master:indeterminate::after {
opacity: 1;
background-color: var(--color-ttg33-mixed);
}
```
## JavaScript
```js
(() => {
const group = document.querySelector('#ttg-33-group');
const master = document.querySelector('#ttg-33-master');
const summary = document.querySelector('#ttg-33-summary');
if (!group || !master || !summary) return;
const kids = () => [...group.querySelectorAll('input[data-child]')];
const sync = () => {
const all = kids();
const on = all.filter((k) => k.checked).length;
master.checked = on === all.length;
master.indeterminate = on > 0 && on < all.length;
// :indeterminate is invisible to assistive tech — mirror it.
master.setAttribute('aria-checked', master.indeterminate ? 'mixed' : String(master.checked));
summary.textContent = on + ' of ' + all.length + ' enabled';
};
master.addEventListener('change', () => {
kids().forEach((k) => { k.checked = master.checked; });
sync();
});
group.addEventListener('change', (e) => { if (e.target.matches('input[data-child]')) sync(); });
sync();
})();
```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 Master Toggle with Indeterminate Tri-State
Source: https://codefronts.com/components/tailwind-css-toggles-switches/tailwind-master-toggle-indeterminate-tri-state/
What demo 13 could not do: a master switch that actually SETS the group, with a real third state. When some children are on, the master goes indeterminate — a dash, not a thumb — using the native pseudo-class and aria-checked="mixed".
## HTML
```html
<section class="ttg-33 w-full min-h-svh grid place-items-center px-5 py-14 bg-ttg33-bg font-ttg33-sans" aria-label="Master toggle with indeterminate state">
<div id="ttg-33-group" class="w-[min(390px,100%)] rounded-[20px] bg-white overflow-hidden border border-ttg33-hair shadow-[0_24px_50px_-34px_oklch(0.4_0.04_265/0.6)]">
<label class="flex items-center gap-5 min-h-16 px-5 py-4 cursor-pointer bg-ttg33-headbg border-b border-ttg33-hair">
<span class="flex-1 flex flex-col gap-0.5">
<b class="text-[15.5px] font-bold text-ttg33-ink">All notifications</b>
<small id="ttg-33-summary" class="text-[12px] text-ttg33-mute">2 of 3 enabled</small>
</span>
<input type="checkbox" id="ttg-33-master" role="switch" aria-checked="mixed" aria-label="All notifications" class="appearance-none shrink-0 w-13 h-7.5 rounded-full bg-ttg33-trackoff relative cursor-pointer transition-colors duration-250 before:content-[''] before:absolute before:top-0.5 before:left-0.5 before:w-6.5 before:h-6.5 before:rounded-full before:bg-white before:shadow-[0_2px_5px_oklch(0.3_0.03_265/0.35)] before:transition-transform before:duration-250 before:ease-[cubic-bezier(0.25,0.9,0.3,1.1)] after:content-[''] after:absolute after:top-1/2 after:left-1/2 after:-mt-[1.5px] after:-ml-[7px] after:w-3.5 after:h-[3px] after:rounded-full after:bg-white after:opacity-0 after:transition-opacity after:duration-200 checked:bg-ttg33-on checked:before:translate-x-[22px] indeterminate:bg-ttg33-mixed indeterminate:before:translate-x-[11px] indeterminate:after:opacity-100 focus-visible:outline-3 focus-visible:outline-ttg33-focus focus-visible:outline-offset-2 motion-reduce:transition-none motion-reduce:before:transition-none">
</label>
<label class="flex items-center gap-5 min-h-15 px-5 py-3.5 cursor-pointer transition-colors duration-200 hover:bg-ttg33-tint has-checked:[&_.ttg33-t]:text-ttg33-ink">
<span class="flex-1 flex flex-col gap-0.5">
<b class="ttg33-t text-[14.5px] font-semibold text-ttg33-dim transition-colors duration-200">Direct messages</b>
<small class="text-[12px] text-ttg33-mute">Push and email</small>
</span>
<input type="checkbox" role="switch" checked data-child aria-label="Direct messages" class="appearance-none shrink-0 w-11 h-6.5 rounded-full bg-ttg33-trackoff relative cursor-pointer transition-colors duration-250 before:content-[''] before:absolute before:top-0.5 before:left-0.5 before:w-[22px] before:h-[22px] before:rounded-full before:bg-white before:shadow-[0_2px_5px_oklch(0.3_0.03_265/0.3)] before:transition-transform before:duration-250 before:ease-[cubic-bezier(0.25,0.9,0.3,1.1)] checked:bg-ttg33-on checked:before:translate-x-[18px] focus-visible:outline-3 focus-visible:outline-ttg33-focus focus-visible:outline-offset-2 motion-reduce:transition-none motion-reduce:before:transition-none">
</label>
<label class="flex items-center gap-5 min-h-15 px-5 py-3.5 cursor-pointer border-t border-ttg33-hair transition-colors duration-200 hover:bg-ttg33-tint has-checked:[&_.ttg33-t]:text-ttg33-ink">
<span class="flex-1 flex flex-col gap-0.5">
<b class="ttg33-t text-[14.5px] font-semibold text-ttg33-dim transition-colors duration-200">Mentions</b>
<small class="text-[12px] text-ttg33-mute">Only when someone @s you</small>
</span>
<input type="checkbox" role="switch" checked data-child aria-label="Mentions" class="appearance-none shrink-0 w-11 h-6.5 rounded-full bg-ttg33-trackoff relative cursor-pointer transition-colors duration-250 before:content-[''] before:absolute before:top-0.5 before:left-0.5 before:w-[22px] before:h-[22px] before:rounded-full before:bg-white before:shadow-[0_2px_5px_oklch(0.3_0.03_265/0.3)] before:transition-transform before:duration-250 before:ease-[cubic-bezier(0.25,0.9,0.3,1.1)] checked:bg-ttg33-on checked:before:translate-x-[18px] focus-visible:outline-3 focus-visible:outline-ttg33-focus focus-visible:outline-offset-2 motion-reduce:transition-none motion-reduce:before:transition-none">
</label>
<label class="flex items-center gap-5 min-h-15 px-5 py-3.5 cursor-pointer border-t border-ttg33-hair transition-colors duration-200 hover:bg-ttg33-tint has-checked:[&_.ttg33-t]:text-ttg33-ink">
<span class="flex-1 flex flex-col gap-0.5">
<b class="ttg33-t text-[14.5px] font-semibold text-ttg33-dim transition-colors duration-200">Product news</b>
<small class="text-[12px] text-ttg33-mute">Monthly, at most</small>
</span>
<input type="checkbox" role="switch" data-child aria-label="Product news" class="appearance-none shrink-0 w-11 h-6.5 rounded-full bg-ttg33-trackoff relative cursor-pointer transition-colors duration-250 before:content-[''] before:absolute before:top-0.5 before:left-0.5 before:w-[22px] before:h-[22px] before:rounded-full before:bg-white before:shadow-[0_2px_5px_oklch(0.3_0.03_265/0.3)] before:transition-transform before:duration-250 before:ease-[cubic-bezier(0.25,0.9,0.3,1.1)] checked:bg-ttg33-on checked:before:translate-x-[18px] focus-visible:outline-3 focus-visible:outline-ttg33-focus focus-visible:outline-offset-2 motion-reduce:transition-none motion-reduce:before:transition-none">
</label>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-ttg33-bg: #edeef4;
--color-ttg33-bg: oklch(0.95 0.006 265);
--color-ttg33-headbg: #f7f8fc;
--color-ttg33-headbg: oklch(0.98 0.004 265);
--color-ttg33-hair: #e8eaf1;
--color-ttg33-hair: oklch(0.93 0.006 265);
--color-ttg33-tint: #f8f9fc;
--color-ttg33-tint: oklch(0.98 0.004 265);
--color-ttg33-ink: #1f2532;
--color-ttg33-ink: oklch(0.26 0.015 265);
--color-ttg33-dim: #737b8d;
--color-ttg33-dim: oklch(0.58 0.018 265);
--color-ttg33-mute: #8f97a7;
--color-ttg33-mute: oklch(0.65 0.015 265);
--color-ttg33-trackoff: #dfe2ea;
--color-ttg33-trackoff: oklch(0.9 0.008 265);
--color-ttg33-on: #2f9e63;
--color-ttg33-on: oklch(0.63 0.16 155);
--color-ttg33-mixed: #4f74e8;
--color-ttg33-mixed: oklch(0.58 0.17 265);
--color-ttg33-focus: #3f63e0;
--color-ttg33-focus: oklch(0.55 0.19 265);
--font-ttg33-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
.ttg-33 {
width: 100%;
min-height: 100svh;
}
.ttg-33 input[type="checkbox"] {
-webkit-appearance: none;
}
/* The mixed dash sits on the thumb, so it needs the thumb's colour, not the track's. */
.ttg-33 #ttg-33-master:indeterminate::after {
opacity: 1;
background-color: var(--color-ttg33-mixed);
}
```
## JavaScript
```js
(() => {
const group = document.querySelector('#ttg-33-group');
const master = document.querySelector('#ttg-33-master');
const summary = document.querySelector('#ttg-33-summary');
if (!group || !master || !summary) return;
const kids = () => [...group.querySelectorAll('input[data-child]')];
const sync = () => {
const all = kids();
const on = all.filter((k) => k.checked).length;
master.checked = on === all.length;
master.indeterminate = on > 0 && on < all.length;
// :indeterminate is invisible to assistive tech — mirror it.
master.setAttribute('aria-checked', master.indeterminate ? 'mixed' : String(master.checked));
summary.textContent = on + ' of ' + all.length + ' enabled';
};
master.addEventListener('change', () => {
kids().forEach((k) => { k.checked = master.checked; });
sync();
});
group.addEventListener('change', (e) => { if (e.target.matches('input[data-child]')) sync(); });
sync();
})();
```How this works
Indeterminate is a property, not an attribute. You cannot set it in HTML; JavaScript writes input.indeterminate = true, and CSS reads it with :indeterminate. Tailwind exposes that as a variant, so the mixed look is authored beside the on and off looks instead of in a separate rule.
Three states, three shapes. Off is a thumb at the left, on is a thumb at the right, mixed is a thumb in the MIDDLE with a dash. Shape, not just colour — which is what makes the tri-state legible at a glance and in greyscale.
The sync is the whole script. Children changing recompute the master (all / none / some); the master changing writes every child. Fourteen lines, and it is the minimum honest amount of JavaScript for a control CSS cannot express, because CSS can read state but never set it.
Make it yours
- Add rows freely — the sync function counts whatever it finds.
- Group by section with several masters, each scoped to its own container.
- Persist with one
localStoragewrite in the same sync function. - Swap the dash for a minus glyph or a partial-fill bar if your design language prefers it.
Gotchas — read before shipping
- Always mirror
indeterminateintoaria-checked="mixed". The property alone is announced as unchecked, which is actively misleading. - Setting
checkedon a child from the master does NOT fire a change event — recompute explicitly rather than relying on the listener. :indeterminateoutranks nothing by itself; order the variants so the mixed state wins over the base, as here.- A master that only reflects state and cannot set it is a status badge. If that is what you want, demo 13 does it with zero JavaScript.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
:indeterminate is universally supported on checkboxes; :has() (Firefox 121+) only drives row emphasis.