Color Scheme
WAGE uses the CSS-native color-scheme property and light-dark() function to handle light and dark contexts. Components don’t need to know whether they’re light or dark — they use semantic tokens, and the tokens resolve automatically based on the inherited color scheme.
How it works
Section titled “How it works”The system has three layers:
- Tokens define two values per semantic property — one for light, one for dark
- Surfaces (or the
surface--darkutility) setcolor-scheme: darkon an element - Components use semantic tokens that auto-resolve based on the inherited scheme
No manual color overrides. No dark-specific CSS selectors. The cascade handles everything.
Semantic tokens with light-dark()
Section titled “Semantic tokens with light-dark()”Semantic tokens in tokens.php use the CSS light-dark() function. The first value is used in light contexts, the second in dark:
'--text-color' => 'light-dark(var(--base-dark), var(--base-light))','--heading-color' => 'light-dark(var(--base-dark), var(--white))','--link-color' => 'light-dark(var(--primary), var(--accent-light))','--border-color' => 'light-dark(var(--neutral-light), color-mix(in oklch, var(--white) 10%, transparent))','--card-bg' => 'light-dark(var(--white), color-mix(in oklch, var(--white) 5%, transparent))',The full list of adaptive tokens:
| Token | Light value | Dark value |
|---|---|---|
--body-background-color | --base-light | --base-dark |
--text-color | --base-dark | --base-light |
--text-color-muted | --neutral | --base-light at 55% |
--heading-color | --base-dark | --white |
--link-color | --primary | --accent-light |
--link-color-hover | --primary-dark | --white |
--border-color | --neutral-light | --white at 10% |
--card-bg | --white | --white at 5% |
--modal-bg | --base-light | --primary-ultra-dark |
Child themes can override any of these in their inc/tokens.php.
The :root default
Section titled “The :root default”The framework sets color-scheme: light on :root when outputting tokens:
:root { color-scheme: light; --text-color: light-dark(var(--base-dark), var(--base-light)); /* ... all tokens ... */}Every page starts in light mode. Dark contexts are opted into per-element.
Switching to dark
Section titled “Switching to dark”There are three ways to put an element into dark mode:
1. Surface classes (most common)
Section titled “1. Surface classes (most common)”Dark surfaces include color-scheme: dark in their CSS definition alongside their visual treatment:
.surface--velvet { color-scheme: dark; background: radial-gradient(...);}Apply to a section:
<section class="hero surface--velvet">Everything inside that section — headings, text, links, borders, cards — automatically uses dark token values. See the Surfaces docs for how to create surfaces.
2. surface--dark utility class
Section titled “2. surface--dark utility class”For flipping the color scheme without any visual treatment (no background, no texture):
/* Defined in core.css */.surface--dark { color-scheme: dark; }Pass it to any component via the classes property:
echo new \Wage\Header( classes: ['surface--dark'] );echo new \Wage\TrustStrip( classes: ['surface--dark'] );Or use it directly in markup:
<div class="surface--dark"> <h2>This heading uses dark tokens</h2> <p>This text uses dark tokens</p></div>3. Inline color-scheme (rare)
Section titled “3. Inline color-scheme (rare)”For one-off cases in component CSS:
.my-dark-card { color-scheme: dark; background-color: var(--primary-ultra-dark);}Locking a component to a color scheme
Section titled “Locking a component to a color scheme”Components can default to a specific scheme via their constructor:
class Header extends Component { public function __construct( array $classes = ['surface--dark'], // dark by default ) { $this->classes = $classes; parent::__construct(); }}This means new \Wage\Header() is dark, but you can override it:
// Dark (default)echo new \Wage\Header();
// Light (override)echo new \Wage\Header( classes: [] );
// Different surfaceecho new \Wage\Header( classes: ['surface--velvet'] );Writing component CSS that adapts
Section titled “Writing component CSS that adapts”The key rule: use semantic tokens, not raw palette tokens.
/* ✅ Correct — adapts automatically */.my-component { color: var(--text-color); border: 1px solid var(--border-color);}.my-component h3 { color: var(--heading-color);}.my-component a { color: var(--link-color);}
/* ❌ Wrong — hardcoded to light mode */.my-component { color: var(--base-dark); border: 1px solid var(--neutral-light);}If a component uses semantic tokens, it works in both light and dark contexts with zero extra CSS.
When to use raw tokens
Section titled “When to use raw tokens”Raw palette tokens are fine for things that don’t change between schemes:
- Brand colors that stay constant:
color: var(--accent) - Decorative elements:
background: var(--primary-dark) - Gradients and overlays where you’re explicitly choosing colors
The rule of thumb: if a property should flip between light and dark, use a semantic token. If it should stay the same regardless, use a raw token.
Local overrides
Section titled “Local overrides”Semantic tokens can be overridden locally on any element:
.my-section { --heading-color: var(--accent);}This overrides the light-dark() default for that element and its descendants, regardless of the color scheme. The heading will be --accent in both light and dark contexts.
Dark mode preview flag
Section titled “Dark mode preview flag”The framework includes a debug flag that forces the entire page into dark mode:
- Enable: visit any page with
?wage=dark-mode - Disable:
?wage=-dark-mode - Persists via cookie across page loads
When active, it sets color-scheme: dark on :root, which flips every light-dark() token on the page. Useful for testing how components look in dark contexts without wrapping them in a dark surface.
See Feature Flags for more on the flag system.
- Always use semantic tokens (
--heading-color,--text-color,--link-color,--border-color) for colors that should adapt between light and dark. - Never write manual dark overrides. No
.surface--dark .my-component h2 { color: white }— letlight-dark()handle it. - Dark surfaces must include
color-scheme: darkin their CSS definition. - One surface = one class. Never stack surface classes. Each surface is self-contained.
- Use
surface--darkfor scheme-only flips. When you need dark tokens but no visual treatment. - Components default via constructor. Set
array $classes = ['surface--dark']for components that are always dark. - Legacy
--surface-dark-*tokens exist but should not be used in new code. Uselight-dark()semantic tokens instead.