Foundations
Theming
Selectors
| Theme | Selector on <html> |
|---|---|
| light | default (:root) or data-mn-theme="light" |
| dark | .dark or data-mn-theme="dark" |
| high-contrast light | data-mn-contrast="more" |
| high-contrast dark | .dark + data-mn-contrast="more" |
| product accent | data-mn-product="<id>" (any element; nest to re-theme a region) |
Token groups
- Surfaces:
--mn-color-bg-{canvas,surface,raised,subtle,muted,emphasis,inverse} - Text:
--mn-color-text-{default,muted,subtle,disabled,inverse,link} - Borders:
--mn-color-border-{subtle,default,strong,input} - Intents
accent | danger | success | warning | info, each withsolid, solid-hover, solid-active, on-solid, subtle, subtle-hover, strong, on-strong, border --mn-color-focus-ring,--mn-focus-ring-width,--mn-color-overlay-scrim- Data:
--mn-color-data-1..5 - Type:
--mn-font-{sans,mono},--mn-text-*,--mn-leading-*,--mn-weight-*,--mn-tracking-* - Layout:
--mn-space-*,--mn-radius-*,--mn-control-*,--mn-layout-*,--mn-z-*,--mn-breakpoint-* - Motion:
--mn-duration-*(0ms under reduced motion),--mn-ease-* - Elevation:
--mn-shadow-{xs,sm,md,lg,xl}
Rule of thumb: solid for fills with on-solid text; strong for text,
icons and rings on surfaces; subtle for tinted backgrounds with strong
text; border for intent outlines.
Guarantees
Every theme × product pair is tested: text ≥ 4.5:1 (7:1 in high contrast),
non-text UI ≥ 3:1, focus ring ≥ 3:1 against surfaces. Run
npm test -w @rookcom/monarch-tokens or read contrast-report.json.
Adding a product
Add registry/products/<id>.json (schema in product.schema.json), run
npm run build. The build generates css/products/<id>.css, the TS
products map and the contrast report; the tests fail if the accent cannot
be made accessible.
Customer brand colours (deriveTheme)
When a customer picks a colour (a booking page, a support portal, a community or careers page), derive the accent from it at runtime instead of trusting it:
import { brandThemeCss, deriveTheme } from '@rookcom/monarch-tokens';
const theme = deriveTheme(workspace.brandColor); // '#rgb' or '#rrggbb'
theme.valid; // false when the value was not a hex colour (the fallback was used)
// A stylesheet that follows the user's theme (.dark, data-mn-contrast="more"):
const css = brandThemeCss(theme, { selector: '.booking-page' });
// Or inline, for a surface that is always one theme:
<div style={theme.modes.light.vars}>…</div>;
- Pure. No DOM, no globals, no cache: the same input always gives the same output, on the server or in the browser.
- Same engine as product accents. Each of the four themes runs
deriveIntent, so a customer colour gets the guarantees above:on-solidon every fill ≥ 4.5:1 (7:1 in high contrast),strong(links, icons, focus ring) on canvas, surface, subtle, muted and the accent's own tints ≥ 4.5:1 (7:1), and a fill that stays visible on the canvas. The fill may be darkened or switched to dark text to get there;modes[id].reportsays what changed, in plain language. - Output.
modes.light | dark | hc-light | hc-dark, each withaccent(HSL roles),reportandvars: every--mn-color-accent-*role plus--mn-color-text-linkand--mn-color-focus-ring. Those two are restated because a custom property'svar()resolves where it is declared (:root), so re-theming a region would otherwise leave links and focus rings on the page accent.deriveTheme(hex, { compat: true })adds the legacy--primaryvariables. - Invalid input never throws. It falls back to
options.fallback, or the Monarch default accent, and setsvalid: false. - Selectors.
brandThemeCssscopes each theme block like the product files:<selector>for light,.dark <selector>/[data-mn-theme="dark"]and[data-mn-contrast="more"](on an ancestor or the element itself), in@layer monarch.tokensunlesslayer: false. parseHexColor(value)validates a colour field before saving it.
Tested in packages/tokens/test/brand.test.ts across more than 1,300
inputs (every 15° of hue at saturations 0–100 and lightness 0–100, plus
white, black, pure yellow and other edge cases) in all four themes.
Overriding
Monarch CSS lives in @layer monarch.*, so any unlayered app rule wins.
Deliberate overrides are allowed but reported by conformance
(overrides/monarch-internals) so they stay visible. Prefer a registry
exception with an owner and expiry.