Home / Snippets / Color & Theming /

Dark mode toggle

A no-JavaScript dark/light mode switch using :has() to toggle custom properties from a checkbox.

Sample card

Toggle the switch above to see the color scheme change in real time, no JS required.

New
colorno-js

Quick implementation

/* Default: dark mode */
:root {
  color-scheme: dark;
  --bg: oklch(0.16 0.01 260);
  --fg: oklch(0.92 0.01 260);
  --surface: oklch(0.22 0.015 260);
}

/* When the toggle checkbox is checked, switch to light */
:root:has(#theme-toggle:checked) {
  color-scheme: light;
  --bg: oklch(0.97 0.005 260);
  --fg: oklch(0.2 0.02 260);
  --surface: oklch(1 0 0);
}

body {
  background: var(--bg);
  color: var(--fg);
  transition: background 0.3s ease, color 0.3s ease;
}

/* Toggle track */
.toggle-track {
  position: relative;
  width: 3rem;
  height: 1.6rem;
  background: oklch(0.35 0.03 260);
  border-radius: 1rem;
  transition: background 0.2s ease;
}
.toggle-track::after {
  content: "";
  position: absolute;
  top: 0.2rem;
  left: 0.2rem;
  width: 1.2rem;
  height: 1.2rem;
  background: oklch(0.95 0 0);
  border-radius: 50%;
  transition: translate 0.2s ease;
}
#theme-toggle:checked ~ label .toggle-track {
  background: oklch(0.6 0.18 270);
}
#theme-toggle:checked ~ label .toggle-track::after {
  translate: 1.4rem 0;
}

Prompt this to your LLM

Paste this into ChatGPT, Claude, or any code-generating model to scaffold the pattern instantly.

Build a pure CSS dark mode toggle. Use a hidden checkbox
with id="theme-toggle" and a styled label acting as the
switch. On :root, define custom properties for --bg, --fg,
and --surface in dark mode (oklch values with low
lightness). Use :root:has(#theme-toggle:checked) to
override those properties with light-mode oklch values.
Set color-scheme: dark by default and color-scheme: light
when checked. Style a pill-shaped toggle track with a
sliding circle using ::after and the translate property.
Add smooth transitions on background and color.

Why this matters

Dark mode is no longer optional for modern sites. The :has() selector, now supported in all major browsers, lets you toggle an entire design system from a single checkbox without any JavaScript. This means the toggle works even if scripts fail to load, providing a resilient user experience. Combined with color-scheme, native form elements and scrollbars adapt automatically.

The logic

Custom properties for background, foreground, and surface colors are declared on :root with dark-mode values. The selector :root:has(#theme-toggle:checked) targets the document root when the checkbox is checked, overriding those properties with light-mode values. Because custom properties cascade, every element using var(--bg) or var(--fg) updates instantly. The color-scheme property tells the browser which palette to use for native UI like scrollbars, inputs, and system dialogs. A transition on background and color smooths the visual switch.

Accessibility & performance

The hidden checkbox should remain in the DOM with opacity: 0 rather than display: none so keyboard users can still tab to it. The <label> must use for to associate with the checkbox for screen readers. Add aria-label="Toggle dark mode" if the visible label text is ambiguous. The color transitions should respect prefers-reduced-motion. For persistence across page loads, a small script reading localStorage is still recommended, but the toggle itself requires no runtime JavaScript.