14 / 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 Material Design 3 Switch with State Layer
A spec-faithful Material 3 switch in utilities: a 52×32 outlined track, a thumb that grows 16 → 24 → 28dp as it slides and presses, a checkmark that fades in on the handle, and the M3 state-layer halo blooming on hover and focus.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="ttg-14 w-full min-h-svh flex flex-col items-center justify-center gap-6 px-5 py-14 bg-ttg14-bg font-ttg14-sans" aria-label="Material Design 3 switch">
<label for="ttg-14-sw" class="flex items-center gap-8 w-[min(380px,100%)] px-6 py-4 rounded-2xl bg-ttg14-surface border border-ttg14-edge cursor-pointer transition-colors duration-200 hover:bg-ttg14-tint">
<span class="flex-1 flex flex-col gap-0.5">
<b class="text-[15px] font-medium text-ttg14-ink">Sync over Wi-Fi only</b>
<small class="text-[12.5px] text-ttg14-mute">Pause large uploads on mobile data</small>
</span>
<input type="checkbox" id="ttg-14-sw" role="switch" checked class="appearance-none shrink-0 w-13 h-8 rounded-2xl relative cursor-pointer bg-ttg14-trackoff border-2 border-ttg14-outline transition-[background-color,border-color] duration-250 checked:bg-ttg14-primary checked:border-ttg14-primary before:content-[''] before:absolute before:top-0 before:left-0 before:w-7 before:h-7 before:rounded-full before:bg-ttg14-outline before:scale-[0.5715] before:transition-[transform,background-color,box-shadow] before:duration-250 before:ease-[cubic-bezier(0.2,0,0,1)] checked:before:bg-white checked:before:translate-x-5 checked:before:scale-[0.857] hover:before:shadow-[0_0_0_10px_--alpha(var(--color-ttg14-primary)/8%)] focus-visible:before:shadow-[0_0_0_10px_--alpha(var(--color-ttg14-primary)/12%)] active:before:scale-100 checked:active:before:translate-x-5 checked:active:before:scale-100 after:content-[''] after:absolute after:top-1.5 after:left-1.5 after:w-4 after:h-4 after:bg-ttg14-primary after:opacity-0 after:translate-x-5 after:scale-50 after:transition-[opacity,transform] after:duration-250 after:ease-[cubic-bezier(0.2,0,0,1)] after:[mask:url(data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2016%2016'%3E%3Cpath%20fill='none'%20stroke='black'%20stroke-width='2.4'%20stroke-linecap='round'%20stroke-linejoin='round'%20d='M3%208.5l3.2%203.2L13%205'/%3E%3C/svg%3E)_center/contain_no-repeat] checked:after:opacity-100 checked:after:scale-100 focus-visible:outline-3 focus-visible:outline-ttg14-primary focus-visible:outline-offset-3 motion-reduce:transition-none motion-reduce:before:transition-none motion-reduce:after:transition-none">
</label>
<p class="text-[12px] tracking-[0.1em] uppercase text-ttg14-mute">M3 spec · growing thumb + state layer</p>
</section><section class="ttg-14 w-full min-h-svh flex flex-col items-center justify-center gap-6 px-5 py-14 bg-ttg14-bg font-ttg14-sans" aria-label="Material Design 3 switch">
<label for="ttg-14-sw" class="flex items-center gap-8 w-[min(380px,100%)] px-6 py-4 rounded-2xl bg-ttg14-surface border border-ttg14-edge cursor-pointer transition-colors duration-200 hover:bg-ttg14-tint">
<span class="flex-1 flex flex-col gap-0.5">
<b class="text-[15px] font-medium text-ttg14-ink">Sync over Wi-Fi only</b>
<small class="text-[12.5px] text-ttg14-mute">Pause large uploads on mobile data</small>
</span>
<input type="checkbox" id="ttg-14-sw" role="switch" checked class="appearance-none shrink-0 w-13 h-8 rounded-2xl relative cursor-pointer bg-ttg14-trackoff border-2 border-ttg14-outline transition-[background-color,border-color] duration-250 checked:bg-ttg14-primary checked:border-ttg14-primary before:content-[''] before:absolute before:top-0 before:left-0 before:w-7 before:h-7 before:rounded-full before:bg-ttg14-outline before:scale-[0.5715] before:transition-[transform,background-color,box-shadow] before:duration-250 before:ease-[cubic-bezier(0.2,0,0,1)] checked:before:bg-white checked:before:translate-x-5 checked:before:scale-[0.857] hover:before:shadow-[0_0_0_10px_--alpha(var(--color-ttg14-primary)/8%)] focus-visible:before:shadow-[0_0_0_10px_--alpha(var(--color-ttg14-primary)/12%)] active:before:scale-100 checked:active:before:translate-x-5 checked:active:before:scale-100 after:content-[''] after:absolute after:top-1.5 after:left-1.5 after:w-4 after:h-4 after:bg-ttg14-primary after:opacity-0 after:translate-x-5 after:scale-50 after:transition-[opacity,transform] after:duration-250 after:ease-[cubic-bezier(0.2,0,0,1)] after:[mask:url(data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2016%2016'%3E%3Cpath%20fill='none'%20stroke='black'%20stroke-width='2.4'%20stroke-linecap='round'%20stroke-linejoin='round'%20d='M3%208.5l3.2%203.2L13%205'/%3E%3C/svg%3E)_center/contain_no-repeat] checked:after:opacity-100 checked:after:scale-100 focus-visible:outline-3 focus-visible:outline-ttg14-primary focus-visible:outline-offset-3 motion-reduce:transition-none motion-reduce:before:transition-none motion-reduce:after:transition-none">
</label>
<p class="text-[12px] tracking-[0.1em] uppercase text-ttg14-mute">M3 spec · growing thumb + state layer</p>
</section>@import "tailwindcss";
@theme {
--color-ttg14-bg: #fdf7ff;
--color-ttg14-bg: oklch(0.98 0.012 310);
--color-ttg14-surface: #ffffff;
--color-ttg14-surface: oklch(1 0 0);
--color-ttg14-tint: #f9f2fc;
--color-ttg14-tint: oklch(0.97 0.014 310);
--color-ttg14-edge: #e7e0ec;
--color-ttg14-edge: oklch(0.91 0.012 310);
--color-ttg14-primary: #6750a4;
--color-ttg14-primary: oklch(0.47 0.13 296);
--color-ttg14-outline: #6f6779;
--color-ttg14-outline: oklch(0.52 0.02 300);
--color-ttg14-trackoff: #e6e0e9;
--color-ttg14-trackoff: oklch(0.91 0.01 310);
--color-ttg14-ink: #1d1b20;
--color-ttg14-ink: oklch(0.23 0.008 300);
--color-ttg14-mute: #6b6572;
--color-ttg14-mute: oklch(0.51 0.018 300);
--font-ttg14-sans: Roboto, 'Plus Jakarta Sans', system-ui, sans-serif;
}
.ttg-14 {
width: 100%;
min-height: 100svh;
}
.ttg-14 input[type="checkbox"] {
-webkit-appearance: none;
}
/* Safari still needs the prefixed mask on the checkmark pseudo-element. */
.ttg-14 input[type="checkbox"]::after {
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round' d='M3 8.5l3.2 3.2L13 5'/%3E%3C/svg%3E") center/contain no-repeat;
}@import "tailwindcss";
@theme {
--color-ttg14-bg: #fdf7ff;
--color-ttg14-bg: oklch(0.98 0.012 310);
--color-ttg14-surface: #ffffff;
--color-ttg14-surface: oklch(1 0 0);
--color-ttg14-tint: #f9f2fc;
--color-ttg14-tint: oklch(0.97 0.014 310);
--color-ttg14-edge: #e7e0ec;
--color-ttg14-edge: oklch(0.91 0.012 310);
--color-ttg14-primary: #6750a4;
--color-ttg14-primary: oklch(0.47 0.13 296);
--color-ttg14-outline: #6f6779;
--color-ttg14-outline: oklch(0.52 0.02 300);
--color-ttg14-trackoff: #e6e0e9;
--color-ttg14-trackoff: oklch(0.91 0.01 310);
--color-ttg14-ink: #1d1b20;
--color-ttg14-ink: oklch(0.23 0.008 300);
--color-ttg14-mute: #6b6572;
--color-ttg14-mute: oklch(0.51 0.018 300);
--font-ttg14-sans: Roboto, 'Plus Jakarta Sans', system-ui, sans-serif;
}
.ttg-14 {
width: 100%;
min-height: 100svh;
}
.ttg-14 input[type="checkbox"] {
-webkit-appearance: none;
}
/* Safari still needs the prefixed mask on the checkmark pseudo-element. */
.ttg-14 input[type="checkbox"]::after {
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round' d='M3 8.5l3.2 3.2L13 5'/%3E%3C/svg%3E") center/contain no-repeat;
}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-ttg14-bg: #fdf7ff;
--color-ttg14-bg: oklch(0.98 0.012 310);
--color-ttg14-surface: #ffffff;
--color-ttg14-surface: oklch(1 0 0);
--color-ttg14-tint: #f9f2fc;
--color-ttg14-tint: oklch(0.97 0.014 310);
--color-ttg14-edge: #e7e0ec;
--color-ttg14-edge: oklch(0.91 0.012 310);
--color-ttg14-primary: #6750a4;
--color-ttg14-primary: oklch(0.47 0.13 296);
--color-ttg14-outline: #6f6779;
--color-ttg14-outline: oklch(0.52 0.02 300);
--color-ttg14-trackoff: #e6e0e9;
--color-ttg14-trackoff: oklch(0.91 0.01 310);
--color-ttg14-ink: #1d1b20;
--color-ttg14-ink: oklch(0.23 0.008 300);
--color-ttg14-mute: #6b6572;
--color-ttg14-mute: oklch(0.51 0.018 300);
--font-ttg14-sans: Roboto, 'Plus Jakarta Sans', system-ui, sans-serif;
}
.ttg-14 {
width: 100%;
min-height: 100svh;
}
.ttg-14 input[type="checkbox"] {
-webkit-appearance: none;
}
/* Safari still needs the prefixed mask on the checkmark pseudo-element. */
.ttg-14 input[type="checkbox"]::after {
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round' d='M3 8.5l3.2 3.2L13 5'/%3E%3C/svg%3E") center/contain no-repeat;
}@import "tailwindcss";
@theme {
--color-ttg14-bg: #fdf7ff;
--color-ttg14-bg: oklch(0.98 0.012 310);
--color-ttg14-surface: #ffffff;
--color-ttg14-surface: oklch(1 0 0);
--color-ttg14-tint: #f9f2fc;
--color-ttg14-tint: oklch(0.97 0.014 310);
--color-ttg14-edge: #e7e0ec;
--color-ttg14-edge: oklch(0.91 0.012 310);
--color-ttg14-primary: #6750a4;
--color-ttg14-primary: oklch(0.47 0.13 296);
--color-ttg14-outline: #6f6779;
--color-ttg14-outline: oklch(0.52 0.02 300);
--color-ttg14-trackoff: #e6e0e9;
--color-ttg14-trackoff: oklch(0.91 0.01 310);
--color-ttg14-ink: #1d1b20;
--color-ttg14-ink: oklch(0.23 0.008 300);
--color-ttg14-mute: #6b6572;
--color-ttg14-mute: oklch(0.51 0.018 300);
--font-ttg14-sans: Roboto, 'Plus Jakarta Sans', system-ui, sans-serif;
}
.ttg-14 {
width: 100%;
min-height: 100svh;
}
.ttg-14 input[type="checkbox"] {
-webkit-appearance: none;
}
/* Safari still needs the prefixed mask on the checkmark pseudo-element. */
.ttg-14 input[type="checkbox"]::after {
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round' d='M3 8.5l3.2 3.2L13 5'/%3E%3C/svg%3E") center/contain no-repeat;
}<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=Roboto: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=Roboto: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.
- Three thumb sizes as three
scale()variants on one base is the port's cleanest translation of a spec:before:scale-[0.5715](16dp),checked:before:scale-[0.857](24dp),active:before:scale-100(28dp). The dp maths sits beside the utility instead of in a comment. --alpha(var(--color-ttg14-primary)/8%)is v4-only and exactly what a state layer needs: the same token at a different opacity, with no second colour entry and nocolor-mixboilerplate.- The
-webkit-maskprefix stays in CSS. Tailwind emits the unprefixed property only, and Safari still needs both on a pseudo-element mask. - UI change from the CSS original: the M3 primary was re-expressed in oklch at
0.47lightness so white-on-primary clears 7:1 (the hex#6750a4sat at 6.2:1 and failed AAA), the supporting text darkened tooklch(0.51), and the row gained a hover tint so the whole control — not just the switch — reads as clickable.
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 Material Design 3 Switch with State Layer
Source: https://codefronts.com/components/tailwind-css-toggles-switches/tailwind-material-design-3-switch-state-layer/
A spec-faithful Material 3 switch in utilities: a 52×32 outlined track, a thumb that grows 16 → 24 → 28dp as it slides and presses, a checkmark that fades in on the handle, and the M3 state-layer halo blooming on hover and focus.
## HTML
```html
<section class="ttg-14 w-full min-h-svh flex flex-col items-center justify-center gap-6 px-5 py-14 bg-ttg14-bg font-ttg14-sans" aria-label="Material Design 3 switch">
<label for="ttg-14-sw" class="flex items-center gap-8 w-[min(380px,100%)] px-6 py-4 rounded-2xl bg-ttg14-surface border border-ttg14-edge cursor-pointer transition-colors duration-200 hover:bg-ttg14-tint">
<span class="flex-1 flex flex-col gap-0.5">
<b class="text-[15px] font-medium text-ttg14-ink">Sync over Wi-Fi only</b>
<small class="text-[12.5px] text-ttg14-mute">Pause large uploads on mobile data</small>
</span>
<input type="checkbox" id="ttg-14-sw" role="switch" checked class="appearance-none shrink-0 w-13 h-8 rounded-2xl relative cursor-pointer bg-ttg14-trackoff border-2 border-ttg14-outline transition-[background-color,border-color] duration-250 checked:bg-ttg14-primary checked:border-ttg14-primary before:content-[''] before:absolute before:top-0 before:left-0 before:w-7 before:h-7 before:rounded-full before:bg-ttg14-outline before:scale-[0.5715] before:transition-[transform,background-color,box-shadow] before:duration-250 before:ease-[cubic-bezier(0.2,0,0,1)] checked:before:bg-white checked:before:translate-x-5 checked:before:scale-[0.857] hover:before:shadow-[0_0_0_10px_--alpha(var(--color-ttg14-primary)/8%)] focus-visible:before:shadow-[0_0_0_10px_--alpha(var(--color-ttg14-primary)/12%)] active:before:scale-100 checked:active:before:translate-x-5 checked:active:before:scale-100 after:content-[''] after:absolute after:top-1.5 after:left-1.5 after:w-4 after:h-4 after:bg-ttg14-primary after:opacity-0 after:translate-x-5 after:scale-50 after:transition-[opacity,transform] after:duration-250 after:ease-[cubic-bezier(0.2,0,0,1)] after:[mask:url(data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2016%2016'%3E%3Cpath%20fill='none'%20stroke='black'%20stroke-width='2.4'%20stroke-linecap='round'%20stroke-linejoin='round'%20d='M3%208.5l3.2%203.2L13%205'/%3E%3C/svg%3E)_center/contain_no-repeat] checked:after:opacity-100 checked:after:scale-100 focus-visible:outline-3 focus-visible:outline-ttg14-primary focus-visible:outline-offset-3 motion-reduce:transition-none motion-reduce:before:transition-none motion-reduce:after:transition-none">
</label>
<p class="text-[12px] tracking-[0.1em] uppercase text-ttg14-mute">M3 spec · growing thumb + state layer</p>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-ttg14-bg: #fdf7ff;
--color-ttg14-bg: oklch(0.98 0.012 310);
--color-ttg14-surface: #ffffff;
--color-ttg14-surface: oklch(1 0 0);
--color-ttg14-tint: #f9f2fc;
--color-ttg14-tint: oklch(0.97 0.014 310);
--color-ttg14-edge: #e7e0ec;
--color-ttg14-edge: oklch(0.91 0.012 310);
--color-ttg14-primary: #6750a4;
--color-ttg14-primary: oklch(0.47 0.13 296);
--color-ttg14-outline: #6f6779;
--color-ttg14-outline: oklch(0.52 0.02 300);
--color-ttg14-trackoff: #e6e0e9;
--color-ttg14-trackoff: oklch(0.91 0.01 310);
--color-ttg14-ink: #1d1b20;
--color-ttg14-ink: oklch(0.23 0.008 300);
--color-ttg14-mute: #6b6572;
--color-ttg14-mute: oklch(0.51 0.018 300);
--font-ttg14-sans: Roboto, 'Plus Jakarta Sans', system-ui, sans-serif;
}
.ttg-14 {
width: 100%;
min-height: 100svh;
}
.ttg-14 input[type="checkbox"] {
-webkit-appearance: none;
}
/* Safari still needs the prefixed mask on the checkmark pseudo-element. */
.ttg-14 input[type="checkbox"]::after {
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round' d='M3 8.5l3.2 3.2L13 5'/%3E%3C/svg%3E") center/contain no-repeat;
}
```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 Material Design 3 Switch with State Layer
Source: https://codefronts.com/components/tailwind-css-toggles-switches/tailwind-material-design-3-switch-state-layer/
A spec-faithful Material 3 switch in utilities: a 52×32 outlined track, a thumb that grows 16 → 24 → 28dp as it slides and presses, a checkmark that fades in on the handle, and the M3 state-layer halo blooming on hover and focus.
## HTML
```html
<section class="ttg-14 w-full min-h-svh flex flex-col items-center justify-center gap-6 px-5 py-14 bg-ttg14-bg font-ttg14-sans" aria-label="Material Design 3 switch">
<label for="ttg-14-sw" class="flex items-center gap-8 w-[min(380px,100%)] px-6 py-4 rounded-2xl bg-ttg14-surface border border-ttg14-edge cursor-pointer transition-colors duration-200 hover:bg-ttg14-tint">
<span class="flex-1 flex flex-col gap-0.5">
<b class="text-[15px] font-medium text-ttg14-ink">Sync over Wi-Fi only</b>
<small class="text-[12.5px] text-ttg14-mute">Pause large uploads on mobile data</small>
</span>
<input type="checkbox" id="ttg-14-sw" role="switch" checked class="appearance-none shrink-0 w-13 h-8 rounded-2xl relative cursor-pointer bg-ttg14-trackoff border-2 border-ttg14-outline transition-[background-color,border-color] duration-250 checked:bg-ttg14-primary checked:border-ttg14-primary before:content-[''] before:absolute before:top-0 before:left-0 before:w-7 before:h-7 before:rounded-full before:bg-ttg14-outline before:scale-[0.5715] before:transition-[transform,background-color,box-shadow] before:duration-250 before:ease-[cubic-bezier(0.2,0,0,1)] checked:before:bg-white checked:before:translate-x-5 checked:before:scale-[0.857] hover:before:shadow-[0_0_0_10px_--alpha(var(--color-ttg14-primary)/8%)] focus-visible:before:shadow-[0_0_0_10px_--alpha(var(--color-ttg14-primary)/12%)] active:before:scale-100 checked:active:before:translate-x-5 checked:active:before:scale-100 after:content-[''] after:absolute after:top-1.5 after:left-1.5 after:w-4 after:h-4 after:bg-ttg14-primary after:opacity-0 after:translate-x-5 after:scale-50 after:transition-[opacity,transform] after:duration-250 after:ease-[cubic-bezier(0.2,0,0,1)] after:[mask:url(data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2016%2016'%3E%3Cpath%20fill='none'%20stroke='black'%20stroke-width='2.4'%20stroke-linecap='round'%20stroke-linejoin='round'%20d='M3%208.5l3.2%203.2L13%205'/%3E%3C/svg%3E)_center/contain_no-repeat] checked:after:opacity-100 checked:after:scale-100 focus-visible:outline-3 focus-visible:outline-ttg14-primary focus-visible:outline-offset-3 motion-reduce:transition-none motion-reduce:before:transition-none motion-reduce:after:transition-none">
</label>
<p class="text-[12px] tracking-[0.1em] uppercase text-ttg14-mute">M3 spec · growing thumb + state layer</p>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-ttg14-bg: #fdf7ff;
--color-ttg14-bg: oklch(0.98 0.012 310);
--color-ttg14-surface: #ffffff;
--color-ttg14-surface: oklch(1 0 0);
--color-ttg14-tint: #f9f2fc;
--color-ttg14-tint: oklch(0.97 0.014 310);
--color-ttg14-edge: #e7e0ec;
--color-ttg14-edge: oklch(0.91 0.012 310);
--color-ttg14-primary: #6750a4;
--color-ttg14-primary: oklch(0.47 0.13 296);
--color-ttg14-outline: #6f6779;
--color-ttg14-outline: oklch(0.52 0.02 300);
--color-ttg14-trackoff: #e6e0e9;
--color-ttg14-trackoff: oklch(0.91 0.01 310);
--color-ttg14-ink: #1d1b20;
--color-ttg14-ink: oklch(0.23 0.008 300);
--color-ttg14-mute: #6b6572;
--color-ttg14-mute: oklch(0.51 0.018 300);
--font-ttg14-sans: Roboto, 'Plus Jakarta Sans', system-ui, sans-serif;
}
.ttg-14 {
width: 100%;
min-height: 100svh;
}
.ttg-14 input[type="checkbox"] {
-webkit-appearance: none;
}
/* Safari still needs the prefixed mask on the checkmark pseudo-element. */
.ttg-14 input[type="checkbox"]::after {
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round' d='M3 8.5l3.2 3.2L13 5'/%3E%3C/svg%3E") center/contain no-repeat;
}
```How this works
The thumb carries the state, not the track. That is the one thing that distinguishes M3 from iOS: unchecked it is a small 16dp outlined dot, checked it grows to 24dp and fills, pressed it reaches 28dp. All three are scale() values against a single 28px element, because animating width would re-rasterise the circle every frame.
The state layer is a shadow, not an element. M3's hover/focus/press feedback is a translucent disc behind the thumb; a 0 0 0 10px spread shadow at 8–12% alpha is the cheapest faithful version. --alpha(var(--color-ttg14-primary)/8%) is v4's way of taking a theme colour down to that opacity without a second token.
The checkmark is a mask, so it is one colour deep. An inline data-URI SVG masks a solid bg-ttg14-primary square, meaning the tick always matches the primary tone and needs no second asset.
Make it yours
- Retheme via the four M3 tokens (
--color-ttg14-primary,-outline,-trackoff,-surface) — that is the whole palette. - Iconless variant: drop the
after:*utilities entirely; the spec allows it. - Dark scheme: swap the token values, change nothing in the markup.
- Error switch: point
--color-ttg14-primaryat your error tone.
Gotchas — read before shipping
- Grow the thumb with
scale()from a fixed base. Animatingwidth/heightre-rasterises a circle on every frame and shows as edge shimmer on low-DPI screens. - The unchecked outline must clear 3:1 against the track — M3's own accessibility note, and the reason the off state is outlined rather than flat.
- State layers stack in the spec (hover + focus + press are separate translucent discs). Approximating with the strongest single layer is fine; rendering all three at once muddies the thumb.
- A data-URI mask inside an arbitrary value must have EVERY space percent-encoded (
%20) and no quotes. A class attribute is space-separated, so one raw space shreds the utility into a dozen junk tokens — the rule never compiles, and the only clue is a checkmark that silently never appears. The unencoded#of a hex colour breaks it the same way, which is why the path here is stroked inblackand coloured by the mask's backdrop.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
Transforms and CSS masks are universal in evergreen browsers; without mask support the tick is invisible and the thumb still fills, which is the spec's iconless variant.