10 CSS Dark Mode Toggles

1 Pure CSS9 Light JSPublished

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 link

Want 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.

10% off at checkout with code
Check ZTM Course

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.

Newsletter

Enjoying these demos? Get new ones by email.

A short email when a new collection ships. Unsubscribe any time.

at

Almost there: check your inbox to confirm.Manage

FAQ

Frequently asked questions

How do I make a dark mode toggle in pure CSS without JavaScript?
The pattern is a hidden checkbox input paired with the modern CSS :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.
How do I prevent the dark mode flash (FOUC) when a page loads?
The flash happens because: page paints in default theme → JS runs → reads localStorage → applies dark class → page repaints in dark. To prevent it, you must apply the theme *before* the first paint. The fix is an inline blocking script at the very top of the document 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.
What's the new CSS light-dark() function and how do I use it?
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.
How do I make a 3-state dark mode toggle (light / dark / system)?
Three radio inputs in a fieldset, each representing a state: 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.
How do I use Tailwind's `darkMode: 'class'` or `[data-theme]` strategy with a toggle?
Tailwind 4.0 ships a new selector strategy that lets you choose any selector for dark mode. Three configurations: (1) 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.
How do I make my dark mode toggle accessible to screen readers?
Five rules: (1) Use a real 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.
How do I animate the dark mode transition smoothly without jank?
Three approaches, increasing in fanciness. (1) **Simple cross-fade**: add 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.
How is this different from theme-toggle (npm), use-dark-mode (npm), or react-toggle-dark-mode?
Three differentiators. **(1) Zero dependencies, zero KB**: these collection demos are vanilla HTML/CSS/JS that work in any framework or no framework. 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.

Related collections

30 CSS Badges preview

30 CSS Badges

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 CSS Banners & Alerts preview

20 CSS Banners & Alerts

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 preview

25 CSS Blockquotes

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.

Search CodeFronts

Loading…