01. Pure CSS Checkbox Hack Toggle (opens in a new tab)
input:checked state propagated upward through the DOM via CSS :has() — no event listeners, no hydration cost.10 production-grade CSS dark mode toggle designs you can copy-paste into any project — pure CSS checkbox hack with :has() (zero JavaScript), animated sun-to-moon micro-interaction with morphing icon, modern light-dark() CSS function boilerplate (Chrome 123+ / Safari 17.5+), 3-way system-sync toggle (light / dark / system — the shadcn / Vercel / Anthropic pattern), localStorage anti-flash template that eliminates FOUC on page load, Tailwind data-theme dashboard toggle with selector-strategy config, neumorphic skeuomorphic tactile switch with soft inset/outset shadows, full-page expanding circular clip-path transition (the iOS-style sweep, View Transitions API + CSS fallback), accessible ARIA toggle with aria-pressed + :focus-visible + live region announcements, and sidebar-bottom fixed toggle for dashboard layouts. Every toggle scoped under a unique .dm-NN prefix so all 10 coexist on this page without style bleed; every @keyframes name namespaced dm-NN-*; every @media (prefers-reduced-motion: reduce) guard already wired in. Each demo ships the full localStorage + prefers-color-scheme + data-theme recipe so you can paste it into any framework — Astro, Next.js, SvelteKit, Eleventy, vanilla — and it just works.
Related5 CSS Variable Dark Mode Systems22 CSS Dark Mode UI47 CSS Toggles & Switches
Course pickPartner: Zero to Mastery
Affiliate linkWant to build production-ready CSS layouts, animations and UI from scratch?
You just browsed 10 dark mode toggles. Learn the Flexbox, Grid and animation thinking behind them, so the next component starts in your head, not in a copy-paste.
Every ZTM course in one membership: $25/month, paid yearly at $299 · 30‑day money-back guarantee
The CSS Bootcamp. We may earn a commission, at no extra cost to you.
input:checked state propagated upward through the DOM via CSS :has() — no event listeners, no hydration cost.light-dark() function — single declarations that adapt both values simultaneously, eliminating duplicate @media blocks entirely.getBoundingClientRect(), and System mode wires a matchMedia listener to the OS.localStorage before CSS loads and applies a data-dm05 attribute to prevent any flash of white on dark pages.data-theme=dark on the root element, flipping every component via attribute-scoped CSS custom properties.clip-path: circle() and CSS custom properties for the origin point.role=switch, aria-checked, keyboard navigation, and a live ARIA debugger panel that shows every attribute update in real time.:has() selector. Place an input type=checkbox anywhere inside your wrapper (visually hidden with clip-path: inset(50%) or opacity: 0; position: absolute), wire it to a visible label via for=id, and write a single CSS rule: .wrapper:has(input:checked) { --bg: #0f172a; --text: #f0f4ff; ... }. The :has() selector evaluates from the parent looking *down* into its descendants, so checking the hidden input redefines every CSS custom property on the wrapper. The whole tree repaints in dark palette with zero JS, zero hydration cost, zero re-render. Browser support: Chrome 105+, Safari 15.4+, Firefox 121+ — combined ~95% of users as of 2026. For older browsers, fall back to the sibling-combinator pattern (input:checked ~ .wrapper). Demo 01 ships the full recipe with sun/moon icon swap on the pill knob.head (before any stylesheet or other JS) that calls document.documentElement.setAttribute('data-theme', localStorage.getItem('theme') || (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')). This runs synchronously, blocks rendering for ~1-2ms (negligible), reads the stored theme + falls back to OS preference, sets the attribute on html before any CSS evaluates. Your CSS then targets [data-theme='dark'] for dark styles. No flash, ever. Demo 05 ships this template — the most copy-pasted dark-mode snippet on the web.light-dark() (CSS Color Module Level 5) is a function that takes two color arguments and returns whichever matches the current color-scheme. Usage: color: light-dark(black, white); background: light-dark(#fff, #0f172a);. To activate it, declare color-scheme: light dark on :root and let prefers-color-scheme drive the choice — OR explicitly set color-scheme: dark (e.g. via a toggle setting color-scheme: dark on the root element) to override. Browser support: Chrome 123+, Safari 17.5+, Firefox 120+ — landed in 2024, broadly safe in 2026. The killer feature is no media queries, no custom properties juggling — just light-dark() everywhere a color goes. Demo 03 ships the boilerplate including the JS that switches color-scheme on toggle for instant theme flip without rebuilding the cascade.light, dark, system. JS writes the chosen value to localStorage.setItem('theme', value) and applies the resolved theme to the root element: if value is 'light' or 'dark', set data-theme directly; if value is 'system', set data-theme to whatever matchMedia('(prefers-color-scheme: dark)').matches returns *and* listen on that media query so OS changes propagate live. On page load, the inline anti-flash script (see FAQ 2) reads the stored value, resolves it the same way, and applies before paint. This is the pattern shadcn UI, Vercel, Anthropic, GitHub, and Tailwind UI all use. Demo 04 ships the full 3-way radio + live OS-listener + persistence recipe in ~40 lines of JS.darkMode: 'class' — toggle by adding class='dark' to the root element; CSS targets .dark .your-class. (2) darkMode: ['selector', '[data-theme="dark"]'] — the modern recommended setup; CSS targets [data-theme='dark'] .your-class; JS just sets document.documentElement.dataset.theme = 'dark'. (3) darkMode: 'media' — automatic OS-preference only, no toggle possible. Use (2) when you need a user-controlled toggle. The Tailwind utilities dark:bg-slate-900 dark:text-white then work automatically. Demo 06 ships a complete dashboard with the data-theme strategy, including the toggle button + localStorage persistence + Tailwind class composition pattern.button type='button' element, not a div with role='button'. The native button handles Enter/Space activation, focus, and screen-reader announcements for free. (2) Add aria-pressed='true' when dark mode is on, aria-pressed='false' when off (NOT aria-checked — that's for checkboxes/radios). The screen reader announces 'Dark mode toggle, pressed' / 'not pressed'. (3) Provide a visible text label like 'Theme' near the toggle, OR use aria-label='Toggle dark mode' on the button itself. Icons alone (sun/moon SVG) need accessible names. (4) Add a visible :focus-visible outline so keyboard users see the focus location. (5) When the user activates the toggle, announce the state change to assistive tech via an aria-live='polite' region — a div with role='status' and aria-live='polite' containing the announcement text. Demo 09 ships all five rules with a :focus-visible ring, aria-pressed toggle, and live region — Lighthouse accessibility score 100.transition: background-color 0.3s, color 0.3s, border-color 0.3s to elements that need to transition. Warning: do NOT add transition: all — it animates layout-affecting properties like width on hover, causing layout thrash. Limit the transition to color properties only. (2) **Disable transitions on first paint**: add a class like theme-transition-pending to the root element initially, then strip it after the page is interactive. Prevents transitions firing on load. (3) **Full-page expanding clip-path** (the iOS-style sweep): use the [View Transitions API](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API) — document.startViewTransition(() => applyTheme(newTheme)). Chrome 111+, Safari 18+. For older browsers, demo 08 ships a CSS-only fallback using clip-path: circle(0 at click-x click-y) animated to circle(150vmax) — gives the same expanding-circle reveal effect cross-browser.theme-toggle is 6KB gzipped, use-dark-mode is 4KB + React-only, react-toggle-dark-mode is 12KB + React-only. For a site with a 80KB JS budget already, adding 4-12KB just for a toggle is wasteful when 40 lines of vanilla code does the same thing. **(2) Patterns, not packages**: this collection teaches you the actual CSS techniques (:has(), light-dark(), data-theme, prefers-color-scheme, View Transitions API) so you can build any variant you need, not just use the maintainer's API. The libraries hide those details behind an opaque hook. **(3) SSR/SSG safe by default**: every demo includes the inline anti-flash script and works with Astro, Next, SvelteKit, Eleventy, etc. The npm packages need wrapper code to be SSR-safe (and use-dark-mode was famously broken in Next.js for two years). Free, copy-paste, MIT-licensed, no signup, works in any project.30 hand-coded CSS badges for status indicators, notifications, membership tiers, live-data displays, and SEO / DevOps / financial dashboards — upload progress, typing indicator, transit line status, Core Web Vitals, ECG heartbeat, CI/CD build pipeline, countdown ring, live price ticker, keycap shortcut, wax seal, conference lanyard, and holographic collectibles. Copy-paste HTML and CSS, WCAG 2.2 accessible, MIT licensed.
20 hand-coded CSS banner and alert bars for e-commerce storefronts, SaaS dashboards, marketing sites, and compliance-bound products. Covers GDPR cookie consent with a real preferences panel, promo bars with countdown timers, free-shipping threshold progress, app-install smart banners, maintenance and incident status bars, newsletter signup bars, age verification gates, limited-stock urgency banners, live event announcements, browser upgrade notices, geo and currency switchers, sticky top announcement bars, bottom cookie bars, inline form validation alerts, icon-aligned alert banners, left-border accent alerts, diagonal stripe promos, full-width hero banners, animated gradient border alerts, and a text-wrapping laboratory that shows five strategies for the unbreakable string that breaks more banners than anything else. Every bar is scoped under a .ba-NN prefix for no-collision pasting, guards prefers-reduced-motion, carries the correct aria-live and role semantics, and ports unchanged to React, Vue, Svelte, Astro, Next.js and Tailwind.
25 CSS blockquotes for article pull quotes, testimonial cards, docs callout admonitions, statement banners, and social-post embeds. Covers giant decorative quotation marks, minimalist hairline and classic left borders, animated gradient and glowing neon borders, glassmorphism and aurora backgrounds, brutalist cards, speech bubbles, thick accent rails, highlighted mark text, newspaper multi-column layouts, an expandable read-more disclosure, a scroll-snap testimonial slider, and one-click copy.