prefers-color-scheme · light-dark() · three-state toggle · FOUC fix
Toggle the theme above to see all components switch. Notice: cards, inputs, buttons, images, and links all adapt. Use the toggle above to switch between System / Light / Dark.
Card background, text, and border all come from CSS custom properties. One set of variables, two themes.
Native form elements need color-scheme or explicit background/color to look correct in dark mode.
Photos should be slightly dimmed in dark mode to reduce eye strain. The image below uses filter: brightness(0.9) automatically.
Body copy adapts automatically. This link uses a consistent accent color in both modes. Muted text uses a lighter grey in dark mode.
This is muted secondary text — darker in light mode, lighter in dark mode.
:root. Clicking the toggle sets data-demo-theme="dark" on the container — the [data-demo-theme="dark"] selector overrides every token in one place. No duplicate CSS, no hunting through stylesheets.
The light-dark() function lets you declare both values in one line — no separate media query needed. But it requires color-scheme: light dark to work.
Every token declared twice. Media query block grows with each new token. Hard to maintain.
Both values inline. No separate media query. Easier to scan, easier to maintain.
color-scheme: light dark is REQUIREDlight-dark() reads the active color-scheme — not directly from prefers-color-scheme. Without color-scheme: light dark on the element (or an ancestor), light-dark() always returns the light value, even if the user has dark mode enabled. Always set color-scheme: light dark on :root first.
Native inputs stay light even when the page is in dark mode — inconsistent, jarring UX.
color-scheme: light dark tells the browser to also switch scrollbars, form inputs, and other native UI to dark mode.
light-dark() now accepts <image> values (URLs, gradients), not just colors:background-image: light-dark(url(logo-dark.png), url(logo-light.png));The full production-ready pattern: CSS variables, system detection, a three-state toggle, localStorage persistence, and the FOUC flash fix.
Without this, users who chose "dark" via your toggle will see a flash of light theme while JavaScript loads. This tiny inline script sets the attribute synchronously before the browser paints.
Update aria-label and aria-pressed via JavaScript to reflect the current mode. Screen readers announce the current state.