17 / 20
All 20 in 20 Tailwind CSS Animated Buttons
- 01Tailwind Neon Glow Button with Pulsing Halo
- 02Tailwind Background Slide Hover Button (Four Directions)
- 03Tailwind Ripple Effect Button with No JavaScript
- 04Tailwind Animated Border Draw Button with Staged Delays
- 05Tailwind 3D Press Effect Button with Keycap Depth
- 06Tailwind Liquid Fill Button with Animated Wave Crest
- 07Tailwind Moving Gradient CTA Button
- 08Tailwind Shine Sweep Hover Button with Gloss Streak
- 09Tailwind Jelly Bounce Button with Elastic Squash
- 10Tailwind Text Flip Button Hover with 3D Rotation
- 11Tailwind Animated CTA Hero Button with Beacon Rings & Particle Burst
- 12Tailwind Submit Button Success State with Checkmark Draw
- 13Tailwind Animated Download Button with Progress Fill
- 14Tailwind Hamburger to Close Button with aria-expanded Variants
- 15Tailwind Add to Cart Button with Flying Product Dot
- 16Tailwind Social Share Buttons with Brand Hover & Copy Link
- 17Tailwind Animated Toggle Switch with peer-checked & has-checked
- 18Tailwind Loading Spinner Inside Button with State Machine
- 19Tailwind Next & Prev Arrow Buttons with Chasing Icons
- 20Tailwind Ghost Button with Cursor-Aware Background Reveal
Tailwind Animated Toggle Switch with peer-checked & has-checked
Three iOS-style switches where the visuals are entirely zero-JS: peer-checked: moves the knob and tints the track, has-checked: highlights the whole row, and the on/off label swaps with peer-checked:hidden. The only JavaScript left announces the change.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="tab-17 w-full min-h-svh grid place-items-center bg-tab17-bg font-tab17-sans px-6 py-10">
<div class="w-[min(380px,100%)]">
<p class="mb-5 text-[0.8rem] font-bold tracking-[0.14em] uppercase text-tab17-mut m-0">Preferences</p>
<div class="flex flex-col gap-2">
<div class="flex items-center justify-between gap-4 p-4 bg-white border-[1.5px] border-tab17-edge rounded-xl transition-[border-color,background-color] duration-300 has-checked:border-tab17-acc/30 has-checked:bg-tab17-tint motion-reduce:transition-none">
<div>
<p class="text-[0.93rem] font-semibold text-tab17-ink m-0">Dark Mode</p>
<p class="mt-0.5 text-[0.78rem] text-tab17-dim m-0">Applies across the whole workspace</p>
</div>
<label class="relative shrink-0 [--on:#7c3aed] cursor-pointer">
<input type="checkbox" class="peer sr-only" data-label="Dark Mode">
<span class="flex items-center w-[50px] h-7 p-[3px] bg-tab17-off rounded-full transition-colors duration-300 peer-checked:bg-[var(--on)] peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-[var(--on)] peer-checked:[&_span]:translate-x-[22px] motion-reduce:transition-none">
<span class="size-[22px] bg-white rounded-full shadow-[0_1px_4px_oklch(0_0_0/0.2)] transition-transform duration-300 ease-[cubic-bezier(0.4,0,0.2,1)] motion-reduce:transition-none"></span>
</span>
<span class="sr-only peer-checked:hidden">Off</span>
<span class="sr-only hidden peer-checked:inline">On</span>
</label>
</div>
<div class="flex items-center justify-between gap-4 p-4 bg-white border-[1.5px] border-tab17-edge rounded-xl transition-[border-color,background-color] duration-300 has-checked:border-tab17-acc/30 has-checked:bg-tab17-tint motion-reduce:transition-none">
<div>
<p class="text-[0.93rem] font-semibold text-tab17-ink m-0">Push Notifications</p>
<p class="mt-0.5 text-[0.78rem] text-tab17-dim m-0">Deploys, alerts and mentions</p>
</div>
<label class="relative shrink-0 [--on:#0e7490] cursor-pointer">
<input type="checkbox" class="peer sr-only" data-label="Push Notifications">
<span class="flex items-center w-[50px] h-7 p-[3px] bg-tab17-off rounded-full transition-colors duration-300 peer-checked:bg-[var(--on)] peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-[var(--on)] peer-checked:[&_span]:translate-x-[22px] motion-reduce:transition-none">
<span class="size-[22px] bg-white rounded-full shadow-[0_1px_4px_oklch(0_0_0/0.2)] transition-transform duration-300 ease-[cubic-bezier(0.4,0,0.2,1)] motion-reduce:transition-none"></span>
</span>
<span class="sr-only peer-checked:hidden">Off</span>
<span class="sr-only hidden peer-checked:inline">On</span>
</label>
</div>
<div class="flex items-center justify-between gap-4 p-4 bg-white border-[1.5px] border-tab17-edge rounded-xl transition-[border-color,background-color] duration-300 has-checked:border-tab17-acc/30 has-checked:bg-tab17-tint motion-reduce:transition-none">
<div>
<p class="text-[0.93rem] font-semibold text-tab17-ink m-0">Auto-Save</p>
<p class="mt-0.5 text-[0.78rem] text-tab17-dim m-0">Every 30 seconds while editing</p>
</div>
<label class="relative shrink-0 [--on:#059669] cursor-pointer">
<input type="checkbox" class="peer sr-only" data-label="Auto-Save" checked>
<span class="flex items-center w-[50px] h-7 p-[3px] bg-tab17-off rounded-full transition-colors duration-300 peer-checked:bg-[var(--on)] peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-[var(--on)] peer-checked:[&_span]:translate-x-[22px] motion-reduce:transition-none">
<span class="size-[22px] bg-white rounded-full shadow-[0_1px_4px_oklch(0_0_0/0.2)] transition-transform duration-300 ease-[cubic-bezier(0.4,0,0.2,1)] motion-reduce:transition-none"></span>
</span>
<span class="sr-only peer-checked:hidden">Off</span>
<span class="sr-only hidden peer-checked:inline">On</span>
</label>
</div>
</div>
<p id="tab17-status" role="status" aria-live="polite" class="sr-only"></p>
</div>
</section><section class="tab-17 w-full min-h-svh grid place-items-center bg-tab17-bg font-tab17-sans px-6 py-10">
<div class="w-[min(380px,100%)]">
<p class="mb-5 text-[0.8rem] font-bold tracking-[0.14em] uppercase text-tab17-mut m-0">Preferences</p>
<div class="flex flex-col gap-2">
<div class="flex items-center justify-between gap-4 p-4 bg-white border-[1.5px] border-tab17-edge rounded-xl transition-[border-color,background-color] duration-300 has-checked:border-tab17-acc/30 has-checked:bg-tab17-tint motion-reduce:transition-none">
<div>
<p class="text-[0.93rem] font-semibold text-tab17-ink m-0">Dark Mode</p>
<p class="mt-0.5 text-[0.78rem] text-tab17-dim m-0">Applies across the whole workspace</p>
</div>
<label class="relative shrink-0 [--on:#7c3aed] cursor-pointer">
<input type="checkbox" class="peer sr-only" data-label="Dark Mode">
<span class="flex items-center w-[50px] h-7 p-[3px] bg-tab17-off rounded-full transition-colors duration-300 peer-checked:bg-[var(--on)] peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-[var(--on)] peer-checked:[&_span]:translate-x-[22px] motion-reduce:transition-none">
<span class="size-[22px] bg-white rounded-full shadow-[0_1px_4px_oklch(0_0_0/0.2)] transition-transform duration-300 ease-[cubic-bezier(0.4,0,0.2,1)] motion-reduce:transition-none"></span>
</span>
<span class="sr-only peer-checked:hidden">Off</span>
<span class="sr-only hidden peer-checked:inline">On</span>
</label>
</div>
<div class="flex items-center justify-between gap-4 p-4 bg-white border-[1.5px] border-tab17-edge rounded-xl transition-[border-color,background-color] duration-300 has-checked:border-tab17-acc/30 has-checked:bg-tab17-tint motion-reduce:transition-none">
<div>
<p class="text-[0.93rem] font-semibold text-tab17-ink m-0">Push Notifications</p>
<p class="mt-0.5 text-[0.78rem] text-tab17-dim m-0">Deploys, alerts and mentions</p>
</div>
<label class="relative shrink-0 [--on:#0e7490] cursor-pointer">
<input type="checkbox" class="peer sr-only" data-label="Push Notifications">
<span class="flex items-center w-[50px] h-7 p-[3px] bg-tab17-off rounded-full transition-colors duration-300 peer-checked:bg-[var(--on)] peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-[var(--on)] peer-checked:[&_span]:translate-x-[22px] motion-reduce:transition-none">
<span class="size-[22px] bg-white rounded-full shadow-[0_1px_4px_oklch(0_0_0/0.2)] transition-transform duration-300 ease-[cubic-bezier(0.4,0,0.2,1)] motion-reduce:transition-none"></span>
</span>
<span class="sr-only peer-checked:hidden">Off</span>
<span class="sr-only hidden peer-checked:inline">On</span>
</label>
</div>
<div class="flex items-center justify-between gap-4 p-4 bg-white border-[1.5px] border-tab17-edge rounded-xl transition-[border-color,background-color] duration-300 has-checked:border-tab17-acc/30 has-checked:bg-tab17-tint motion-reduce:transition-none">
<div>
<p class="text-[0.93rem] font-semibold text-tab17-ink m-0">Auto-Save</p>
<p class="mt-0.5 text-[0.78rem] text-tab17-dim m-0">Every 30 seconds while editing</p>
</div>
<label class="relative shrink-0 [--on:#059669] cursor-pointer">
<input type="checkbox" class="peer sr-only" data-label="Auto-Save" checked>
<span class="flex items-center w-[50px] h-7 p-[3px] bg-tab17-off rounded-full transition-colors duration-300 peer-checked:bg-[var(--on)] peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-[var(--on)] peer-checked:[&_span]:translate-x-[22px] motion-reduce:transition-none">
<span class="size-[22px] bg-white rounded-full shadow-[0_1px_4px_oklch(0_0_0/0.2)] transition-transform duration-300 ease-[cubic-bezier(0.4,0,0.2,1)] motion-reduce:transition-none"></span>
</span>
<span class="sr-only peer-checked:hidden">Off</span>
<span class="sr-only hidden peer-checked:inline">On</span>
</label>
</div>
</div>
<p id="tab17-status" role="status" aria-live="polite" class="sr-only"></p>
</div>
</section>@import "tailwindcss";
@theme {
--color-tab17-bg: #f8fafc;
--color-tab17-bg: oklch(0.98 0.004 250);
--color-tab17-ink: #1e293b;
--color-tab17-ink: oklch(0.28 0.03 258);
--color-tab17-mut: #64748b;
--color-tab17-mut: oklch(0.55 0.03 255);
--color-tab17-dim: #94a3b8;
--color-tab17-dim: oklch(0.72 0.03 255);
--color-tab17-edge: #e2e8f0;
--color-tab17-edge: oklch(0.92 0.01 250);
--color-tab17-off: #cbd5e1;
--color-tab17-off: oklch(0.85 0.02 250);
--color-tab17-acc: #7c3aed;
--color-tab17-acc: oklch(0.53 0.25 295);
--color-tab17-tint: #faf7ff;
--color-tab17-tint: oklch(0.98 0.01 300);
--font-tab17-sans: 'Space Grotesk', system-ui, sans-serif;
}
.tab-17 {
width: 100%;
min-height: 100svh;
}
.tab-17 ::selection {
background: var(--color-tab17-acc);
color: #fff;
}@import "tailwindcss";
@theme {
--color-tab17-bg: #f8fafc;
--color-tab17-bg: oklch(0.98 0.004 250);
--color-tab17-ink: #1e293b;
--color-tab17-ink: oklch(0.28 0.03 258);
--color-tab17-mut: #64748b;
--color-tab17-mut: oklch(0.55 0.03 255);
--color-tab17-dim: #94a3b8;
--color-tab17-dim: oklch(0.72 0.03 255);
--color-tab17-edge: #e2e8f0;
--color-tab17-edge: oklch(0.92 0.01 250);
--color-tab17-off: #cbd5e1;
--color-tab17-off: oklch(0.85 0.02 250);
--color-tab17-acc: #7c3aed;
--color-tab17-acc: oklch(0.53 0.25 295);
--color-tab17-tint: #faf7ff;
--color-tab17-tint: oklch(0.98 0.01 300);
--font-tab17-sans: 'Space Grotesk', system-ui, sans-serif;
}
.tab-17 {
width: 100%;
min-height: 100svh;
}
.tab-17 ::selection {
background: var(--color-tab17-acc);
color: #fff;
}(function () {
var status = document.getElementById('tab17-status');
var inputs = document.querySelectorAll('.tab-17 input[type="checkbox"]');
if (!status || !inputs.length) return;
inputs.forEach(function (input) {
input.addEventListener('change', function () {
status.textContent = (input.dataset.label || 'Setting') + (input.checked ? ' on' : ' off');
});
});
})();(function () {
var status = document.getElementById('tab17-status');
var inputs = document.querySelectorAll('.tab-17 input[type="checkbox"]');
if (!status || !inputs.length) return;
inputs.forEach(function (input) {
input.addEventListener('change', function () {
status.textContent = (input.dataset.label || 'Setting') + (input.checked ? ' on' : ' off');
});
});
})();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-tab17-bg: #f8fafc;
--color-tab17-bg: oklch(0.98 0.004 250);
--color-tab17-ink: #1e293b;
--color-tab17-ink: oklch(0.28 0.03 258);
--color-tab17-mut: #64748b;
--color-tab17-mut: oklch(0.55 0.03 255);
--color-tab17-dim: #94a3b8;
--color-tab17-dim: oklch(0.72 0.03 255);
--color-tab17-edge: #e2e8f0;
--color-tab17-edge: oklch(0.92 0.01 250);
--color-tab17-off: #cbd5e1;
--color-tab17-off: oklch(0.85 0.02 250);
--color-tab17-acc: #7c3aed;
--color-tab17-acc: oklch(0.53 0.25 295);
--color-tab17-tint: #faf7ff;
--color-tab17-tint: oklch(0.98 0.01 300);
--font-tab17-sans: 'Space Grotesk', system-ui, sans-serif;
}
.tab-17 {
width: 100%;
min-height: 100svh;
}
.tab-17 ::selection {
background: var(--color-tab17-acc);
color: #fff;
}@import "tailwindcss";
@theme {
--color-tab17-bg: #f8fafc;
--color-tab17-bg: oklch(0.98 0.004 250);
--color-tab17-ink: #1e293b;
--color-tab17-ink: oklch(0.28 0.03 258);
--color-tab17-mut: #64748b;
--color-tab17-mut: oklch(0.55 0.03 255);
--color-tab17-dim: #94a3b8;
--color-tab17-dim: oklch(0.72 0.03 255);
--color-tab17-edge: #e2e8f0;
--color-tab17-edge: oklch(0.92 0.01 250);
--color-tab17-off: #cbd5e1;
--color-tab17-off: oklch(0.85 0.02 250);
--color-tab17-acc: #7c3aed;
--color-tab17-acc: oklch(0.53 0.25 295);
--color-tab17-tint: #faf7ff;
--color-tab17-tint: oklch(0.98 0.01 300);
--font-tab17-sans: 'Space Grotesk', system-ui, sans-serif;
}
.tab-17 {
width: 100%;
min-height: 100svh;
}
.tab-17 ::selection {
background: var(--color-tab17-acc);
color: #fff;
}<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=Space+Grotesk:wght@400;500;600;700&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=Space+Grotesk:wght@400;500;600;700&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- This is the port that removes the most JavaScript: status text and row highlight both became variants, leaving a controller that does nothing but announce. If you drop the live region you have a genuinely zero-JS component.
peer-checked:[&_span]:translate-x-[22px]reaches the knob through the track — one class where the CSS version neededinput:checked + .track .knob.- UI change from the CSS original: each row gained a one-line description, the visible "On/Off" text became screen-reader-only (the switch itself already communicates state visually), and the focus ring now rides the visible track rather than the hidden input.
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 Animated Toggle Switch with peer-checked & has-checked
Source: https://codefronts.com/motion/tailwind-css-animated-buttons/tailwind-animated-toggle-switch-button/
Three iOS-style switches where the visuals are entirely zero-JS: peer-checked: moves the knob and tints the track, has-checked: highlights the whole row, and the on/off label swaps with peer-checked:hidden. The only JavaScript left announces the change.
## HTML
```html
<section class="tab-17 w-full min-h-svh grid place-items-center bg-tab17-bg font-tab17-sans px-6 py-10">
<div class="w-[min(380px,100%)]">
<p class="mb-5 text-[0.8rem] font-bold tracking-[0.14em] uppercase text-tab17-mut m-0">Preferences</p>
<div class="flex flex-col gap-2">
<div class="flex items-center justify-between gap-4 p-4 bg-white border-[1.5px] border-tab17-edge rounded-xl transition-[border-color,background-color] duration-300 has-checked:border-tab17-acc/30 has-checked:bg-tab17-tint motion-reduce:transition-none">
<div>
<p class="text-[0.93rem] font-semibold text-tab17-ink m-0">Dark Mode</p>
<p class="mt-0.5 text-[0.78rem] text-tab17-dim m-0">Applies across the whole workspace</p>
</div>
<label class="relative shrink-0 [--on:#7c3aed] cursor-pointer">
<input type="checkbox" class="peer sr-only" data-label="Dark Mode">
<span class="flex items-center w-[50px] h-7 p-[3px] bg-tab17-off rounded-full transition-colors duration-300 peer-checked:bg-[var(--on)] peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-[var(--on)] peer-checked:[&_span]:translate-x-[22px] motion-reduce:transition-none">
<span class="size-[22px] bg-white rounded-full shadow-[0_1px_4px_oklch(0_0_0/0.2)] transition-transform duration-300 ease-[cubic-bezier(0.4,0,0.2,1)] motion-reduce:transition-none"></span>
</span>
<span class="sr-only peer-checked:hidden">Off</span>
<span class="sr-only hidden peer-checked:inline">On</span>
</label>
</div>
<div class="flex items-center justify-between gap-4 p-4 bg-white border-[1.5px] border-tab17-edge rounded-xl transition-[border-color,background-color] duration-300 has-checked:border-tab17-acc/30 has-checked:bg-tab17-tint motion-reduce:transition-none">
<div>
<p class="text-[0.93rem] font-semibold text-tab17-ink m-0">Push Notifications</p>
<p class="mt-0.5 text-[0.78rem] text-tab17-dim m-0">Deploys, alerts and mentions</p>
</div>
<label class="relative shrink-0 [--on:#0e7490] cursor-pointer">
<input type="checkbox" class="peer sr-only" data-label="Push Notifications">
<span class="flex items-center w-[50px] h-7 p-[3px] bg-tab17-off rounded-full transition-colors duration-300 peer-checked:bg-[var(--on)] peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-[var(--on)] peer-checked:[&_span]:translate-x-[22px] motion-reduce:transition-none">
<span class="size-[22px] bg-white rounded-full shadow-[0_1px_4px_oklch(0_0_0/0.2)] transition-transform duration-300 ease-[cubic-bezier(0.4,0,0.2,1)] motion-reduce:transition-none"></span>
</span>
<span class="sr-only peer-checked:hidden">Off</span>
<span class="sr-only hidden peer-checked:inline">On</span>
</label>
</div>
<div class="flex items-center justify-between gap-4 p-4 bg-white border-[1.5px] border-tab17-edge rounded-xl transition-[border-color,background-color] duration-300 has-checked:border-tab17-acc/30 has-checked:bg-tab17-tint motion-reduce:transition-none">
<div>
<p class="text-[0.93rem] font-semibold text-tab17-ink m-0">Auto-Save</p>
<p class="mt-0.5 text-[0.78rem] text-tab17-dim m-0">Every 30 seconds while editing</p>
</div>
<label class="relative shrink-0 [--on:#059669] cursor-pointer">
<input type="checkbox" class="peer sr-only" data-label="Auto-Save" checked>
<span class="flex items-center w-[50px] h-7 p-[3px] bg-tab17-off rounded-full transition-colors duration-300 peer-checked:bg-[var(--on)] peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-[var(--on)] peer-checked:[&_span]:translate-x-[22px] motion-reduce:transition-none">
<span class="size-[22px] bg-white rounded-full shadow-[0_1px_4px_oklch(0_0_0/0.2)] transition-transform duration-300 ease-[cubic-bezier(0.4,0,0.2,1)] motion-reduce:transition-none"></span>
</span>
<span class="sr-only peer-checked:hidden">Off</span>
<span class="sr-only hidden peer-checked:inline">On</span>
</label>
</div>
</div>
<p id="tab17-status" role="status" aria-live="polite" class="sr-only"></p>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tab17-bg: #f8fafc;
--color-tab17-bg: oklch(0.98 0.004 250);
--color-tab17-ink: #1e293b;
--color-tab17-ink: oklch(0.28 0.03 258);
--color-tab17-mut: #64748b;
--color-tab17-mut: oklch(0.55 0.03 255);
--color-tab17-dim: #94a3b8;
--color-tab17-dim: oklch(0.72 0.03 255);
--color-tab17-edge: #e2e8f0;
--color-tab17-edge: oklch(0.92 0.01 250);
--color-tab17-off: #cbd5e1;
--color-tab17-off: oklch(0.85 0.02 250);
--color-tab17-acc: #7c3aed;
--color-tab17-acc: oklch(0.53 0.25 295);
--color-tab17-tint: #faf7ff;
--color-tab17-tint: oklch(0.98 0.01 300);
--font-tab17-sans: 'Space Grotesk', system-ui, sans-serif;
}
.tab-17 {
width: 100%;
min-height: 100svh;
}
.tab-17 ::selection {
background: var(--color-tab17-acc);
color: #fff;
}
```
## JavaScript
```js
(function () {
var status = document.getElementById('tab17-status');
var inputs = document.querySelectorAll('.tab-17 input[type="checkbox"]');
if (!status || !inputs.length) return;
inputs.forEach(function (input) {
input.addEventListener('change', function () {
status.textContent = (input.dataset.label || 'Setting') + (input.checked ? ' on' : ' off');
});
});
})();
```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 Animated Toggle Switch with peer-checked & has-checked
Source: https://codefronts.com/motion/tailwind-css-animated-buttons/tailwind-animated-toggle-switch-button/
Three iOS-style switches where the visuals are entirely zero-JS: peer-checked: moves the knob and tints the track, has-checked: highlights the whole row, and the on/off label swaps with peer-checked:hidden. The only JavaScript left announces the change.
## HTML
```html
<section class="tab-17 w-full min-h-svh grid place-items-center bg-tab17-bg font-tab17-sans px-6 py-10">
<div class="w-[min(380px,100%)]">
<p class="mb-5 text-[0.8rem] font-bold tracking-[0.14em] uppercase text-tab17-mut m-0">Preferences</p>
<div class="flex flex-col gap-2">
<div class="flex items-center justify-between gap-4 p-4 bg-white border-[1.5px] border-tab17-edge rounded-xl transition-[border-color,background-color] duration-300 has-checked:border-tab17-acc/30 has-checked:bg-tab17-tint motion-reduce:transition-none">
<div>
<p class="text-[0.93rem] font-semibold text-tab17-ink m-0">Dark Mode</p>
<p class="mt-0.5 text-[0.78rem] text-tab17-dim m-0">Applies across the whole workspace</p>
</div>
<label class="relative shrink-0 [--on:#7c3aed] cursor-pointer">
<input type="checkbox" class="peer sr-only" data-label="Dark Mode">
<span class="flex items-center w-[50px] h-7 p-[3px] bg-tab17-off rounded-full transition-colors duration-300 peer-checked:bg-[var(--on)] peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-[var(--on)] peer-checked:[&_span]:translate-x-[22px] motion-reduce:transition-none">
<span class="size-[22px] bg-white rounded-full shadow-[0_1px_4px_oklch(0_0_0/0.2)] transition-transform duration-300 ease-[cubic-bezier(0.4,0,0.2,1)] motion-reduce:transition-none"></span>
</span>
<span class="sr-only peer-checked:hidden">Off</span>
<span class="sr-only hidden peer-checked:inline">On</span>
</label>
</div>
<div class="flex items-center justify-between gap-4 p-4 bg-white border-[1.5px] border-tab17-edge rounded-xl transition-[border-color,background-color] duration-300 has-checked:border-tab17-acc/30 has-checked:bg-tab17-tint motion-reduce:transition-none">
<div>
<p class="text-[0.93rem] font-semibold text-tab17-ink m-0">Push Notifications</p>
<p class="mt-0.5 text-[0.78rem] text-tab17-dim m-0">Deploys, alerts and mentions</p>
</div>
<label class="relative shrink-0 [--on:#0e7490] cursor-pointer">
<input type="checkbox" class="peer sr-only" data-label="Push Notifications">
<span class="flex items-center w-[50px] h-7 p-[3px] bg-tab17-off rounded-full transition-colors duration-300 peer-checked:bg-[var(--on)] peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-[var(--on)] peer-checked:[&_span]:translate-x-[22px] motion-reduce:transition-none">
<span class="size-[22px] bg-white rounded-full shadow-[0_1px_4px_oklch(0_0_0/0.2)] transition-transform duration-300 ease-[cubic-bezier(0.4,0,0.2,1)] motion-reduce:transition-none"></span>
</span>
<span class="sr-only peer-checked:hidden">Off</span>
<span class="sr-only hidden peer-checked:inline">On</span>
</label>
</div>
<div class="flex items-center justify-between gap-4 p-4 bg-white border-[1.5px] border-tab17-edge rounded-xl transition-[border-color,background-color] duration-300 has-checked:border-tab17-acc/30 has-checked:bg-tab17-tint motion-reduce:transition-none">
<div>
<p class="text-[0.93rem] font-semibold text-tab17-ink m-0">Auto-Save</p>
<p class="mt-0.5 text-[0.78rem] text-tab17-dim m-0">Every 30 seconds while editing</p>
</div>
<label class="relative shrink-0 [--on:#059669] cursor-pointer">
<input type="checkbox" class="peer sr-only" data-label="Auto-Save" checked>
<span class="flex items-center w-[50px] h-7 p-[3px] bg-tab17-off rounded-full transition-colors duration-300 peer-checked:bg-[var(--on)] peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-[var(--on)] peer-checked:[&_span]:translate-x-[22px] motion-reduce:transition-none">
<span class="size-[22px] bg-white rounded-full shadow-[0_1px_4px_oklch(0_0_0/0.2)] transition-transform duration-300 ease-[cubic-bezier(0.4,0,0.2,1)] motion-reduce:transition-none"></span>
</span>
<span class="sr-only peer-checked:hidden">Off</span>
<span class="sr-only hidden peer-checked:inline">On</span>
</label>
</div>
</div>
<p id="tab17-status" role="status" aria-live="polite" class="sr-only"></p>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tab17-bg: #f8fafc;
--color-tab17-bg: oklch(0.98 0.004 250);
--color-tab17-ink: #1e293b;
--color-tab17-ink: oklch(0.28 0.03 258);
--color-tab17-mut: #64748b;
--color-tab17-mut: oklch(0.55 0.03 255);
--color-tab17-dim: #94a3b8;
--color-tab17-dim: oklch(0.72 0.03 255);
--color-tab17-edge: #e2e8f0;
--color-tab17-edge: oklch(0.92 0.01 250);
--color-tab17-off: #cbd5e1;
--color-tab17-off: oklch(0.85 0.02 250);
--color-tab17-acc: #7c3aed;
--color-tab17-acc: oklch(0.53 0.25 295);
--color-tab17-tint: #faf7ff;
--color-tab17-tint: oklch(0.98 0.01 300);
--font-tab17-sans: 'Space Grotesk', system-ui, sans-serif;
}
.tab-17 {
width: 100%;
min-height: 100svh;
}
.tab-17 ::selection {
background: var(--color-tab17-acc);
color: #fff;
}
```
## JavaScript
```js
(function () {
var status = document.getElementById('tab17-status');
var inputs = document.querySelectorAll('.tab-17 input[type="checkbox"]');
if (!status || !inputs.length) return;
inputs.forEach(function (input) {
input.addEventListener('change', function () {
status.textContent = (input.dataset.label || 'Setting') + (input.checked ? ' on' : ' off');
});
});
})();
```How this works
The status text stopped being JavaScript's job. The CSS original used JS to write "On"/"Off" and to toggle a row class. Both are states of a checkbox, and v4 can read a checkbox from a sibling (peer-checked:) or from an ancestor (has-checked:) — so both moved into the markup, and the controller shrank to a single aria-live announcement.
The input stays real and stays reachable. sr-only rather than hidden, so it keeps its role, its keyboard behaviour and its focus — which the visible track then borrows via peer-focus-visible:.
The on-colour is per-row data. Each toggle declares [--on:#…] inline and the track reads var(--on): one utility, three values, no variant triplication.
Make it yours
- Knob travel: keep
peer-checked:[&_span]:translate-x-[22px]equal to track width minus knob width minus padding. - Icons in the knob: it is a real
span, so put an SVG inside it. - Per-row colour: edit the inline
[--on:#…]; nothing else needs to change.
Gotchas — read before shipping
peer-checked:compiles to a sibling combinator, so the input must stay an immediate previous sibling of the track. Wrapping either in a div silently kills every toggle.- Never hide the input with
display:none— it leaves the control unreachable by keyboard and invisible to assistive tech.sr-onlyis the only correct hiding. has-checked:on the row needs the input to be a descendant of that row; it is the ancestor-side read of the same state, not a replacement forpeer-checked:.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
:has() drives the row highlight (Baseline 2023); without it the switches still work and only the row tint is lost.