@magnet-js/theme
v0.2.0
Published
System-preference-driven theme selection for Magnet.
Readme
@magnet/theme
System-preference-driven theme selection for Magnet.
@magnet/theme scores your registered themes against the user's system
preferences — light/dark, contrast, forced-colors, and inverted-colors — and
applies the winner, watching for changes. It gives you a computed manager (user
override + resolved theme), an anti-FOUC head script, optional localStorage
persistence, theme-color meta management, and per-theme details payloads for
your own tokens or metadata.
Install
JSR
deno add jsr:@magnet/themenpm via JSR
npx jsr add @magnet/themenpm
npm install @magnet-js/themeUsage
Themes are plain data: theme(id, tags, options?) builds a descriptor with an
identifier, the system conditions it serves, and an optional theme-color and
details payload:
import { tc39 as signal } from "@magnet/signal";
import { theme, themeManager, ThemeTag } from "@magnet/theme";
import { magnet } from "@magnet/ui";
const m = magnet({ window, ...signal });
const themes = [
theme("paper", [ThemeTag.Light], { color: "#fafafa" }),
theme("ink", [ThemeTag.Dark], { color: "#282c34" }),
];
const manager = themeManager(m, themes, document.documentElement, undefined, {
localStorage: {},
});
manager.get(); // the effective theme (object) after scoring
manager.selected.get(); // the user's pick, or null for system selection
manager.id = "ink"; // explicit pick (persisted)
manager.id = null; // back to system selectionThe active theme's identifier is written to the element's data-theme attribute
and color-scheme is set on it, so plain CSS drives the look:
[data-theme="ink"] {
--background: #282c34;
}Anti-FOUC head script
headScript(themes, options) returns an inline <script> string that applies
the winning theme before first paint, using the same scoring algorithm as
the manager. Its options are a Pick of the manager's (attributeName,
localStorage, priority) plus a CSP nonce, so one options vocabulary drives
both:
import { headScript } from "@magnet/theme";
// in your SSR template:
// <head>${headScript(themes, { nonce: "abc123" })}</head>Scoring
Themes are scored against the active system conditions: power-of-two axis
weights from priority (default
[ColorForcing, ColorInversion, Contrast,
Lightness] — hard display overrides
first, contrast next, color preference last), more specific matches outranking
less specific ones, and array order breaking ties. Every theme is evaluated; the
highest scorer wins. Nothing matches → the first registered theme applies.
watch (default: all axes) subscribes to the media queries and re-resolves on
change; disableTransitions (default: true) suppresses CSS transitions during
theme changes by injecting a *{transition:none!important} style for one task.
Per-theme details payloads
Each theme can carry an arbitrary payload of your own (D, via the details
option) — design tokens, fonts, marketing metadata. The manager ignores it; read
it back from the resolved theme:
const ink = theme("ink", [ThemeTag.Dark], {
color: "#282c34",
details: { font: "IBM Plex Sans", accent: "#61afef" },
});
// manager.get().details?.accentSince descriptors are plain objects, a JSON manifest maps to them almost directly.
License
MIT © 2026 Fernando G. Vilar.
