
Dark Mode UI with prefers-color-scheme Root Strategy
Published
A CSS dark-mode UI is a surface whose theme system, tokens, and toggle contract are production primitives — not a colour swap. These 22 hand-coded patterns cover prefers-color-scheme, [data-theme], color-mix(), light-dark(), WCAG 2.4.13 focus rings, a System / Light / Dark switcher, and full UI surfaces — sidebars, dashboards, modals, code editors, OLED cards, layered slate. Every demo is scoped under .dmu-NN, uses pure-CSS :has(:checked) toggles that flip on any OS, guards motion behind prefers-reduced-motion, and drops into React, Vue, Svelte, or Astro without framework lock-in.
Related5 CSS Variable Dark Mode Systems10 CSS Dark Mode Toggles12 CSS Neon Designs

Published

[data-theme] attribute wins over the media query, and a pure-CSS toggle — a visually hidden <input type="checkbox"> read by :has(:checked) — wins over both. Includes the nested-media flip-back rule, the one line that stops the toggle from silently doing nothing for visitors whose OS already matches your default.Published

--gray-0 … --gray-12, --accent-500), then derive every semantic token — surface, elevated surface, hairline, ink, muted ink, accent wash — with color-mix(in oklab, …). Two cards sit on opposite surfaces using the identical token names to prove the derivation adapts instead of inverting.Published

color-scheme: light dark and declare every token once as light-dark(<light>, <dark>). The function resolves against the element's effective colour scheme at compute time, so changing color-scheme anywhere re-themes the whole subtree — which makes the toggle a single property change instead of a second token block.Published

Published

Published

<details> disclosure supplies the open state, the <summary> doubles as the trigger and the dismiss control, and a fixed blurred backdrop sits between them. The sheet uses an elevated surface with an inner-glow hairline — the detail that separates a dark modal from a grey box — and animates in with @starting-style so the entrance costs nothing at runtime.Published

outline in a per-theme focus token: bright cyan on dark, deep violet on light, each measured at 3:1 or better against the surface it lands on. An annotation ribbon lists what was tuned and the contrast value it hit.Published

prefers-color-scheme through; Light and Dark pin explicit token sets that win over the media query. Three radios, three labels, one sliding thumb, zero JavaScript — plus a live badge that reports what the OS is currently asking for, so the difference between "System" and "Dark" is visible rather than theoretical.Published

Published

#000 and every layer above it climbs a short surface ramp — #0a0a0a, #141414, #1c1c1c — instead of relying on shadows that a true-black substrate swallows. The result is deeper contrast, a measurably cheaper screen and an interface that looks like it was designed for the display rather than ported to it.PublishedUpdated

PublishedUpdated

PublishedUpdated

prefers-reduced-motion.PublishedUpdated

PublishedUpdated

PublishedUpdated

PublishedUpdated

color-mix() rather than two hard-coded greys, and a selected row marked by an accent edge plus a checked box — never colour alone. Row density, hover feedback and focus rings are all tuned for the case that actually happens: forty rows, scanned quickly, on a dim screen.PublishedUpdated

PublishedUpdated

box-shadow in the accent hue, layered under a tight inner highlight so the button looks lit rather than blurred. A live countdown ticks in tabular figures (fifteen lines of vanilla JS, no theming logic), and the ambient pulse behind the card stops dead under prefers-reduced-motion.PublishedUpdated

PublishedUpdated

PublishedUpdated
Tweak the exact look in our visual generators — no signup, instant copy-paste.
prefers-color-scheme — declare your tokens once on the root, then redeclare only the tokens inside @media (prefers-color-scheme: dark). The UI adapts to the operating system automatically with zero script. Second, [data-theme] attribute override — pair the media query with .root[data-theme="dark"] and .root[data-theme="light"] rules that WIN over the media query so a user preference can beat the OS. For an interactive toggle without any JavaScript, use a visually-hidden <input type="checkbox"> plus a <label> and read the state with :has(#your-id:checked) on the ancestor — the whole flow is pure CSS. Demos 01, 02, and 04 in this collection ship each pattern as a reference implementation. The gotcha every toggle build hits: on the visitor's OS-matching default, the :has(:checked) rule and the OS default land on the same colours, so the first click looks like it did nothing. Fix by adding a nested rule inside the @media block that restores the opposite base when :has(:checked) matches — the flip-back pattern makes the toggle work in both directions on any device.#000 costs less battery, gives infinite contrast and reads as intentional. Demo 11 uses this substrate with a short elevation ramp (#0a0a0a, #141414, #1c1c1c) for cards, rows and hover states, because a shadow on true black would be swallowed by the substrate — surface tokens do the depth work instead. For non-OLED displays, a very dark grey (roughly #0a0a0b to #111114) usually reads better: it softens the contrast with pure white text, and layering progressively lighter greys lets you show elevation without shadows. The right choice depends on your primary audience and device mix — dashboards used on desktop monitors typically pick dark grey; reading apps and streaming interfaces used on OLED phones and TVs pick true black. Every demo in this collection declares its ground colour as a --void or --bg token so you can swap between the two by editing one value.color-mix(in oklab, base 90%, white 10%) collapses that to one: define primitive tokens once, then compose semantic tokens as mixes relative to currentColor or a base neutral, and the mix adapts automatically to the surface it lands on. Perceptual mixing in oklab keeps mid-mixes from going muddy the way sRGB does. The newer light-dark() CSS function goes further — set color-scheme: light dark on the root and every colour can be written as light-dark(#f4f4f5, #0a0a0a). The function reads the effective color-scheme at compute time, so a single [data-theme] swap on the root cascades through every declaration with zero media queries. Baseline widely available in Chromium since May 2024, Safari since March 2024, Firefox 120+ — production-safe for a codebase with a modern-browser floor. Demo 03 shows the color-mix primer, demo 04 shows light-dark(), and both wrap their bleeding-edge features in @supports gates with sRGB fallbacks declared first.#0a0a0a. Two rules keep this reliable. First, calibrate a distinct --focus-ring token per theme rather than reusing the primary accent — bright cyan (#22d3ee) works on most dark surfaces because oklch(83% 0.13 200) sits high in lightness. Second, use outline: 2px solid var(--focus-ring); outline-offset: 2px; rather than a box-shadow ring — outlines respect outline-offset cleanly and never get clipped by overflow: hidden. Demo 08 is the reference implementation with an inline audit ribbon calling out the contrast values for every interactive control on the surface. Pair with SC 1.4.11 (Non-text Contrast, 3:1) for the borders around inputs and buttons in their idle state, and SC 1.4.3 (Contrast Minimum, 4.5:1) for body text against the dark background.name attribute, one label per state, and :has() selectors on the surface for the two explicit states — the System option is the DEFAULT and needs no override rule. The cascade goes: @media (prefers-color-scheme: dark) supplies the base when nothing is checked (System = follow OS); :has(#light:checked) overrides both media queries with explicit light tokens; :has(#dark:checked) overrides with explicit dark tokens. The subtle bug every implementation hits: inside @media (prefers-color-scheme: dark), add a :has(#light:checked) restore-light rule that WINS over the media query, and inside @media (prefers-color-scheme: light) add a :has(#dark:checked) restore-dark rule. Without those two nested rules, a visitor on the matching OS gets stuck on their default when they select the opposite. Demo 09 ships the reference wiring with an annotation ribbon labelling each selector and its role in the cascade. Bonus: pair the switcher with a JavaScript one-liner that syncs <meta name="theme-color"> so the browser chrome (address bar on mobile, title bar on macOS) tracks the theme.[data-theme="dark"], and the whole page flips. Three complementary fixes. First, put a tiny synchronous script BEFORE the first paint that reads localStorage.getItem('theme') and stamps document.documentElement.setAttribute('data-theme', value) — put it in the <head> above every stylesheet link, keep it under 200 bytes, and skip any imports. Second, add <meta name="color-scheme" content="light dark"> in the head — the browser paints the default background-color and scrollbars in dark grey before your CSS loads, which eliminates the white flash even without JavaScript. Third, ensure your @media (prefers-color-scheme: dark) block sets color-scheme: dark on the root so native form controls, scrollbars and text-selection adopt dark styling immediately. Combining all three eliminates flash on cold loads, warm loads, view transitions and Safari's bfcache restores.--surface-0 through --surface-3 with 8-15 unit lightness steps in oklab — replaces box-shadow depth on true-black substrates and reads as physical layering. Second, a focus-ring token distinct from the primary accent, calibrated for 3:1 contrast against the dark surface (WCAG 2.4.13). Third, an --accent-glow token that pairs with --accent for emissive CTAs (demo 20's phosphor glow, demo 22's ember pulse) so glow intensity flips off in light mode. Fourth, a semantic status palette (--ok, --warn, --err) that reads correctly on both grounds — saturated mid-tones that pass on white often fail on #0a0a0a and need a lightness lift. Fifth, a --divider-strong and --divider-subtle pair so hairlines stay visible on the dark substrate without ringing every card. Sixth, tabular-nums typography via font-variant-numeric: tabular-nums for any numeric display — column-aligned figures read as data on dark surfaces where a lighter-weight body copy would blur. Demos 03, 06, 17, 18 and 19 each show one or more of these primitives in production context..dmu-NN root so it drops into any framework without leaking. Tailwind v3 users pair the demo with the darkMode: 'class' config so dark: variants respect the same [data-theme] attribute the demo listens to; Tailwind v4 uses @custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *)) in your CSS-first config. React and Next.js apps typically ship the theme decision as a Context provider that writes data-theme on document.documentElement — the demo CSS reads the attribute regardless of who sets it. Vue 3 users bind :data-theme="theme" on the layout root and useDark() from VueUse writes it. Svelte does it with bind:data-theme on the <svelte:element> root. Astro islands (client:visible) work with the pure-CSS demos as-is; for the interactive three-state switcher, hydrate a small island around demo 09's markup. SolidJS, Qwik, and Angular signals all map to the same [data-theme] attribute pattern. The light-dark() demo (04) is the most future-proof — with color-scheme: light dark on the root, a single attribute swap cascades through every colour declaration with zero framework glue.9 hand-coded CSS 3D scenes built with real transforms, perspective, and preserve-3d — iridescent flip cards, a midnight coverflow carousel, kinetic tilt pricing cards, a page-turn flipbook, a Bauhaus drag-cube navigator, a modular synth control panel, a luxury hover-flip product showcase, a 5×5 spinning cube matrix, and a 60-orb DNA double helix. No WebGL, no libraries.
22 hand-coded brutalist and neubrutalist CSS designs covering the full spectrum of the aesthetic — from the anti-design brutalism of 2014-2018 (raw system fonts, zero border radius, monochrome density, visible structure) through to the neubrutalism of 2021-present (bright pastel fills, thick black borders, hard offset shadows with zero blur, rounded blocks). The neubrutalist half ships SaaS pricing cards with sticker badges, a pastel admin dashboard, signup forms, rotated feature cards, an offset-shadow navbar, a playful button set, a native dialog modal, a testimonial wall, blog cards, chunky toggles, a landing hero, an image gallery, stats panels and a rounded footer. The brutalist half covers product grids, raw form controls, a monochrome data dashboard, a portfolio index, indie SaaS pricing, Windows 95 desktop chrome, editorial long-read and a streetwear catalog. Every design is scoped under a .brt-NN prefix for no-collision pasting, guards prefers-reduced-motion, and ports unchanged to React, Vue, Svelte, Astro, Next.js and Tailwind.
16 production CSS frosted glass effect designs with live demos and copy-paste code — a full-viewport login modal, dashboard overlay panel, sticky glassmorphism navbar with blur-on-scroll via IntersectionObserver, sidebar navigation menu, media-player footer overlay, animated login/signup form, split-panel contact form UI, profile info card, credit card payment UI, product display card, crypto finance dashboard, Apple-style weather widget, glass music player UI, translucent calendar widget, frosted glass modal popup, and a VisionOS-style floating app dock. Every demo uses backdrop-filter blur + saturation 180% on a colorful animated backdrop for real glassmorphism, with a @supports fallback so browsers without support stay readable. Scoped under .fg-NN, copy-paste ready.