Dark Mode
DelightStack uses the CSS light-dark() function for dark mode. This is a native CSS approach — no JavaScript theme provider, no class toggling on every element, and no flash of unstyled content.
How It Works
Section titled “How It Works”Every color token in the design system specifies both its light and dark values using light-dark():
--color-bg: light-dark(#ffffff, #0a0a0a);--color-text: light-dark(#171717, #fafafa);--color-action: light-dark(#2563eb, #3b82f6);The browser selects the correct value based on the color-scheme property set on the element or its ancestors. That is the entire mechanism — a single CSS property controls which branch of every light-dark() call is used.
Setting Up Dark Mode
Section titled “Setting Up Dark Mode”-
Set
color-schemeon:root::root {color-scheme: light dark;}With
color-scheme: light dark, the browser automatically follows the user’s system preference (prefers-color-scheme). -
Define your tokens using
light-dark()::root {color-scheme: light dark;--color-bg: light-dark(#ffffff, #0a0a0a);--color-text: light-dark(#171717, #fafafa);/* ... rest of your tokens */} -
That is it. All DelightStack components will automatically render in the correct mode.
Controlling the Mode
Section titled “Controlling the Mode”Follow System Preference (Default)
Section titled “Follow System Preference (Default)”:root { color-scheme: light dark;}The browser uses the user’s OS preference. When they switch their system theme, your app follows.
Force Light Mode
Section titled “Force Light Mode”:root { color-scheme: light;}Force Dark Mode
Section titled “Force Dark Mode”:root { color-scheme: dark;}Toggle with JavaScript
Section titled “Toggle with JavaScript”To let users override the system preference, change the color-scheme property at runtime:
function setMode(mode) { document.documentElement.style.colorScheme = mode;}
setMode('dark'); // Force darksetMode('light'); // Force lightsetMode('light dark'); // Follow systemThe ThemeToggle Component
Section titled “The ThemeToggle Component”DelightStack includes a ThemeToggle component that handles all of this for you — including persisting the user’s choice in localStorage:
<script> import { ThemeToggle } from '@delightstack/components';</script>
<ThemeToggle />The ThemeToggle cycles between three states: light, dark, and auto (system preference). It updates the color-scheme property on <html> and remembers the user’s choice across sessions.
Using light-dark() in Your Own CSS
Section titled “Using light-dark() in Your Own CSS”You can use the same light-dark() function in your own custom styles to stay consistent with the design system:
.custom-card { background: light-dark(#f8fafc, #1e293b); border: 1px solid light-dark(rgb(0 0 0 / 0.08), rgb(255 255 255 / 0.08)); color: light-dark(#334155, #e2e8f0);}
.highlight { background: light-dark( color-mix(in oklch, var(--color-action) 10%, transparent), color-mix(in oklch, var(--color-action) 20%, transparent) );}Respecting User Preferences
Section titled “Respecting User Preferences”prefers-color-scheme
Section titled “prefers-color-scheme”When color-scheme is set to light dark, the browser automatically respects the user’s prefers-color-scheme setting. You can also use this media query directly for styles that fall outside the design token system:
@media (prefers-color-scheme: dark) { .custom-element { /* Dark-specific styles */ filter: brightness(0.9); }}prefers-reduced-motion
Section titled “prefers-reduced-motion”DelightStack components respect prefers-reduced-motion automatically, disabling or simplifying animations for users who prefer reduced motion. If you add custom animations, follow the same pattern:
.custom-animation { transition: transform var(--duration-normal) var(--ease-default);}
@media (prefers-reduced-motion: reduce) { .custom-animation { transition: none; }}Dark Mode Design Patterns
Section titled “Dark Mode Design Patterns”DelightStack components follow specific patterns in dark mode that are worth understanding if you build custom components:
Elevation via Borders, Not Shadows
Section titled “Elevation via Borders, Not Shadows”Shadows are invisible on dark backgrounds. Instead, dark mode uses stronger borders and subtle inset highlights:
.elevated-panel { background: var(--color-surface-2); border: 1px solid var(--border-elevated-2); box-shadow: var(--shadow-md); /* Light: real shadow. Dark: subtle inset top highlight */}Tinted Text on Colored Backgrounds
Section titled “Tinted Text on Colored Backgrounds”Text on action-colored backgrounds uses a barely-tinted off-white, not pure white. On hover, it transitions to pure white — a subtle “revelation” effect:
.action-button { background: var(--color-action); color: var(--color-action-text); /* tinted off-white */}
.action-button:hover { background: var(--color-action-hover); color: var(--color-action-text-hover); /* pure white */}Backdrop Blur for Focus
Section titled “Backdrop Blur for Focus”Modal backdrops use backdrop-filter: blur() to focus attention. Small popovers and menus skip the blur to avoid distraction:
.modal-backdrop { background: var(--color-backdrop); backdrop-filter: blur(var(--backdrop-blur));}Preventing Flash of Incorrect Theme
Section titled “Preventing Flash of Incorrect Theme”If you allow users to choose a theme (stored in localStorage), apply the preference in a blocking <script> in <head> to prevent a flash of the wrong theme on page load:
<head> <script> const stored = localStorage.getItem('theme'); if (stored === 'dark') { document.documentElement.style.colorScheme = 'dark'; } else if (stored === 'light') { document.documentElement.style.colorScheme = 'light'; } </script></head>