Dark Mode
Hydrateless supports dark mode out of the box. There's nothing to configure for the common case, and there is no separate dark stylesheet: every semantic color token is a light-dark() pair, and the browser picks the right side based on the element's color-scheme.
How it works
theme.css declares the color scheme:
:root {
color-scheme: light dark;
}
[data-theme='light'] {
color-scheme: light;
}
[data-theme='dark'] {
color-scheme: dark;
}And semantic.css defines each color once:
:root {
--hl-bg: light-dark(var(--hl-gray-50), var(--hl-gray-950));
--hl-fg: light-dark(var(--hl-gray-900), var(--hl-gray-50));
/* ... */
}With color-scheme: light dark, the browser resolves light-dark() from the user's operating-system preference. Native form controls, scrollbars, and the default canvas color follow the same setting, so the whole page agrees.
Automatic (OS preference)
Import the CSS and you're done:
@import 'hydrateless';Pages follow prefers-color-scheme with no extra markup.
Manual override
To let users pick a theme regardless of their OS setting, set data-theme on the root element:
<html data-theme="dark">
...
</html>data-theme accepts light or dark. It works by narrowing color-scheme to a single value, which forces every light-dark() token to that side. Remove the attribute to fall back to automatic behavior.
Because the override is just color-scheme, you can also scope it. A data-theme="dark" on a sidebar makes only the sidebar dark:
<aside data-theme="dark">
<!-- Every component inside renders with the dark token values. -->
</aside>Toggling at runtime
A theme toggle is a single attribute flip:
function setTheme(theme) {
// 'light' | 'dark' | undefined (follow the OS)
if (theme) document.documentElement.dataset.theme = theme;
else delete document.documentElement.dataset.theme;
}To persist the choice and avoid a flash of the wrong theme, apply it before first paint:
<script>
const saved = localStorage.getItem('theme');
if (saved) document.documentElement.dataset.theme = saved;
</script>function saveTheme(theme) {
setTheme(theme);
if (theme) localStorage.setItem('theme', theme);
else localStorage.removeItem('theme');
}Customizing dark colors
There is no dark block to override. Redefine the token with both values:
:root {
--hl-bg: light-dark(hsl(0deg 0% 100%), hsl(222deg 47% 11%));
--hl-primary: light-dark(hsl(221deg 80% 50%), hsl(221deg 80% 65%));
}If you only want to change the dark side, keep the light value by referencing the palette:
:root {
--hl-surface: light-dark(hsl(0deg 0% 100%), var(--hl-gray-800));
}Shadows and the dialog scrim are also light-dark() pairs (--hl-shadow-*, --hl-scrim), so dark surfaces get deeper shadows automatically.
Forced colors
Under Windows High Contrast (forced-colors: active) the semantic tokens map to system colors (CanvasText, Highlight, Canvas), and components keep visible borders and use Highlight for selected states. See Accessibility.
Testing both schemes
In Chromium DevTools, use Rendering > Emulate CSS media feature prefers-color-scheme to flip the OS preference without touching your system settings. The end-to-end suite does the same with Playwright's emulateMedia({ colorScheme: 'dark' }).