Color schemes
A Jx design can ship light and dark variants of its tokens, follow the visitor's OS preference by default, and still let the visitor force either scheme with a switcher. The whole mechanism builds on named media breakpoints — no separate theme system.
Declaring scheme support
Add a
scheme query
to
$media
in
project.json
— an entry whose value is exactly a
prefers-color-scheme
query:
{
"$media": {
"--md": "(min-width: 768px)",
"--dark": "(prefers-color-scheme: dark)"
}
}
Then override any design token (or any style property) per scheme with an
@--dark
block. Convention: author the light values as the base and the dark values as overrides.
{
"style": {
"--color-bg": "#ffffff",
"--color-text": "#18181b",
"@--dark": {
"--color-bg": "#0a0a0a",
"--color-text": "#fafafa"
}
}
}
@--dark
blocks work in component and element styles too, exactly like responsive breakpoints:
{
"tagName": "div",
"style": {
"boxShadow": "0 2px 8px rgba(0,0,0,0.12)",
"@--dark": { "boxShadow": "0 2px 8px rgba(0,0,0,0.5)" }
}
}
What declaring a scheme query does
Declaring a scheme query opts the site into the forced-scheme contract:
Every
@--darkblock is emitted twice: once inside@media (prefers-color-scheme: dark)(applies in auto mode), and once under:root[data-color-scheme="dark"](applies when the scheme is forced ). Both copies are specificity-neutral, so your cascade is unchanged.color-scheme: light darkis declared on:root, with forced-mode overrides — native form controls and scrollbars follow along.A tiny inline script is injected at the top of
<head>that restores the visitor's persisted choice before first paint, so a forced scheme never flashes.
The visitor override contract
Two constants make up the contract:
| Constant | Where | Meaning |
|---|---|---|
data-color-scheme |
attribute on
<html> |
"light"
or
"dark"
forces that scheme; absent = auto |
jx-color-scheme |
localStorage
key |
persisted forced scheme; absent = auto |
A switcher only needs to keep the two in sync. Cycling auto → light → dark:
const next = { auto: "light", light: "dark", dark: "auto" }[current];
if (next === "auto") {
localStorage.removeItem("jx-color-scheme");
document.documentElement.removeAttribute("data-color-scheme");
} else {
localStorage.setItem("jx-color-scheme", next);
document.documentElement.setAttribute("data-color-scheme", next);
}
The pre-paint script (injected automatically) reads the storage key on the next load, so the choice sticks across pages and visits.
Only
pure
scheme queries participate:
(prefers-color-scheme: dark)
with nothing else attached. A compound query like
(prefers-color-scheme: dark) and (min-width: 768px)
compiles as a plain media query and does not respond to the forced attribute.
Studio shows an Auto/Light/Dark preview toggle in the tab bar whenever the project declares a scheme query, and the design token editor can author the
@--dark
values for you.