hyperhive/docs/web-ui/css-vars.md
iris 56e7a309e8 swarm-ui: respect prefers-color-scheme for a light theme default
colors.css shipped one fixed dark (Catppuccin Mocha) palette with no
light variant and no OS/browser-preference detection, so anyone
without a stylix-managed session (a phone browser, a future PWA
install) got the dark palette regardless of their light-mode
preference. This is an accessibility gap, not a cosmetic one - some
people need light for low-vision/contrast reasons, others need dark
for photosensitivity, the same reasoning that already gates
prefers-reduced-motion elsewhere.

Add a Catppuccin Latte row behind `@media (prefers-color-scheme:
light)`, ported through the identical base16 slot mapping (including
the existing sky-over-teal base0C substitution) so a swap between the
two rows never changes what a slot means. A stylix-generated
colors.css replaces the file wholesale with a single unconditional
:root block and has no media query in it, so a themed deployment is
unaffected either way.

Verified with headless chromium screenshots forcing both
prefers-color-scheme values against the built swarm-ui bundle - both
render with correct contrast, and the default (no forced preference)
render matches the light row.
2026-08-18 23:29:20 +02:00

9.5 KiB
Raw Blame History

CSS custom properties (theme variables)

Colour variables live in two standalone stylesheets, split so a theme swap touches only the first:

  1. colors.css — the 16 --base00--base0F base16 slots. This is the entire theme swap contract. A generator (e.g. one fed a stylix base16 scheme, which is natively base00base0F) replaces only this file.
  2. theme.css — the semantic layer: --bg, --fg, --purple, … derived from the base16 slots (--bg: var(--base00) etc.). This is what the app references and it never changes on a swap.

Both files live in frontend/packages/shared/src/. Per-page stylesheets (common.css, dashboard.css, flow.css, logs.css, agent.css) reference the semantic names and must not redeclare them or reach for the raw --baseNN slots directly. (base.css holds only the shared body typography — it references the palette but no longer defines it.)

colors.css ships two rows of the same 16 slots: an unconditional Catppuccin Mocha (dark) default, and a Catppuccin Latte (light) row gated behind @media (prefers-color-scheme: light) — both ported through the identical slot mapping, so which row is active never changes what a slot means. This is the OS/browser-level default only; it's what a client with no more specific theme decision gets (see "Theme swapping" below for how a stylix deployment or a future per-user override supersede it).

Both are deliberately standalone, not @imported into the page bundles: each package re-exports them (src/{colors,theme}.css@import "@hive/shared/…") so esbuild emits its own dist/static/colors.css + dist/static/theme.css, and every page links them first (colors.css then theme.css) ahead of the page CSS. Note theme.css does not @import colors.css — that would bake the slots back into it; they're two separate output files so a swap replaces just colors.css. (Load order doesn't actually affect resolution — custom properties resolve at computed-value time — but colors.css is linked first for clarity.)

Semantic palette (theme.css)

Each semantic var derives from a base16 slot (or, for the three that have no clean slot, a color-mix() over base16 — pixel-identical under the default palette). Default hexes shown are Catppuccin Mocha.

Variable Derives from Default hex Use
--bg base00 #1e1e2e page background
--bg-elev base01 #181825 elevated surfaces: floating dropdowns, popovers
--border base02 #313244 general borders, hover/active backgrounds
--purple-dim base03 #45475a subtle borders, terminal chrome, badge backgrounds
--fg base05 #cdd6f4 primary text colour
--red base08 #f38ba8 errors, fail state
--amber base09 #fab387 warnings, pending / running state
--yellow base0A #f9e2af flash messages, mild warnings
--green base0B #a6e3a1 success, ok state
--cyan base0C #89dceb tool-use events, info accents
--blue base0D #89b4fa links, interactive accent (distinct from cyan)
--purple base0E #cba6f7 accent — active tabs, links, highlights
--pink base0F #f5c2e7 thinking events
--crust mix(base00 58%, #000) #11111b terminal / code block background (below --bg)
--muted mix(base05 55.5%, base00) #7f849c secondary / dimmed text
--subtext0 mix(base05 77.7%, base00) #a6adc8 toolbar/status text; dimmer than --fg, lighter than --muted

Common mistakes

The Catppuccin colour names do not map 1:1 to the variable names. Variables to avoid (undefined — they will silently resolve to transparent / inherited):

Wrong Correct
--text --fg
--mauve --purple
--surface0 --bg-elev (float bg) or --border (border/hover)
--surface1 --border
--surface2 --purple-dim
--overlay0, --overlay1 --muted
--base, --mantle --bg, --bg-elev

Usage guide

Floating menus and dropdowns (e.g. agent context menu, tabbar overflow):

background: var(--bg-elev);
border: 1px solid var(--purple-dim);

Hover / active state backgrounds:

background: var(--border);

Active tab text / accent elements:

color: var(--purple);

Muted / meta text:

color: var(--muted);

Error / warning / success badges:

color: var(--red); /* error */
color: var(--amber); /* warning / running */
color: var(--green); /* ok */

Theme swapping — the base16 contract

The swap interface is colors.css — the 16 base16 slots, not our semantic names. A theme generator (e.g. one reading a stylix base16 scheme) overrides only colors.css; the semantic layer in theme.css derives everything else, so the whole UI re-themes with nothing else to template or regenerate. The base16 slot → semantic mapping is internal to theme.css (the "Derives from" column above) — a generator never needs to know our var names, and theme.css + the page bundles stay untouched.

Two rows, one contract. colors.css ships both a dark default (Catppuccin Mocha, unconditional) and a light default (Catppuccin Latte, behind @media (prefers-color-scheme: light)) — a theme generator that overrides the file wholesale (e.g. the stylix path, which writes a single unconditional :root block with no media query) supersedes both rows at once, same as today.

colors.css base16 slot defaults (Catppuccin Mocha, dark default):

Slot Default Standard base16 role Mapped to
base00 #1e1e2e default bg --bg, (darkened) --crust
base01 #181825 lighter bg --bg-elev
base02 #313244 selection/surface --border
base03 #45475a comments/dim surface --purple-dim
base04 #585b70 dark foreground (unused; kept for completeness)
base05 #cdd6f4 default foreground --fg, (blended) --muted/--subtext0
base06 #f5e0dc light foreground (unused)
base07 #b4befe lightest (unused)
base08 #f38ba8 red --red
base09 #fab387 orange --amber
base0A #f9e2af yellow --yellow
base0B #a6e3a1 green --green
base0C #89dceb cyan --cyan (our sky; Catppuccin's base0C is teal)
base0D #89b4fa blue --blue
base0E #cba6f7 magenta --purple
base0F #f5c2e7 extra accent --pink

Notes for theme authors:

  • --crust, --muted, --subtext0 have no own slot — they're derived via color-mix() over base16 (--crust = a darkened base00; --muted/--subtext0 = base05base00 blends). They still track a swap automatically; no generator action needed.
  • A standard Catppuccin base16 scheme uses teal for base0C; we default it to sky (#89dceb) to preserve the historical accent. A stylix Catppuccin scheme will shift --cyan to teal — that's the operator's chosen scheme, working as intended.
  • base04/base06/base07 aren't consumed by a semantic var today; they're kept at their standard values so the 16-slot contract is complete (a partial scheme override still works).