Light/Dark Theming
Zero-class automatic theming with light-dark()
The Traditional Approach
Most frameworks require explicit dark mode classes:
<!-- You have to write both -->
<div class="bg-white text-gray-900 dark:bg-gray-900 dark:text-white">
Content
</div>
This doubles your class count and is easy to forget.
Shift CSS: Zero-Class Theming
Shift CSS uses the native CSS light-dark() function. Components automatically adapt:
<!-- Just write once -->
<div s-surface="raised">
Content automatically adapts to light/dark mode
</div>
How It Works
Semantic tokens are defined with both light and dark values:
:root {
color-scheme: light dark;
--s-surface-base: light-dark(
var(--s-neutral-50), /* Light mode */
var(--s-neutral-950) /* Dark mode */
);
--s-text-primary: light-dark(
var(--s-neutral-900),
var(--s-neutral-50)
);
}
The browser automatically selects the appropriate value based on prefers-color-scheme.
Forcing a Theme
To override the system preference:
/* Force light mode */
:root {
color-scheme: light;
}
/* Force dark mode */
:root {
color-scheme: dark;
}
Or with JavaScript:
// Toggle dark mode
document.documentElement.style.colorScheme = 'dark';
Semantic Color Tokens
| Token | Light | Dark | Usage |
|---|---|---|---|
--s-surface-base | neutral-50 | neutral-950 | Default backgrounds |
--s-surface-raised | neutral-100 | neutral-900 | Cards, modals |
--s-surface-sunken | neutral-200 | neutral-800 | Inputs, wells |
--s-text-primary | neutral-900 | neutral-50 | Main text |
--s-text-secondary | neutral-600 | neutral-400 | Muted text |
--s-border-default | neutral-200 | neutral-800 | Borders |
Auto-Contrast Text
Button components automatically calculate readable text color using OKLCH relative color syntax:
[s-btn="primary"] {
--_bg: var(--s-primary-500);
/* Auto-contrast: light text on dark bg, dark text on light bg */
--_color: oklch(from var(--_bg) calc(l < 0.6 ? 0.98 : 0.15) 0.01 h);
}
This ensures WCAG AA compliant contrast regardless of the primary hue chosen.