hyperhive/docs/web-ui/css-vars.md
iris 72e087b086 swarm-ui: fix WCAG contrast failures in the light-theme accent slots
argus review: the literal Catppuccin Latte accent hexes (base08-0F)
fail badly as StatusChip fill-text (green/amber/red/yellow on
--purple-dim/base03: 1.4:1-3:1, need 4.5:1) and even as plain text on
--bg/base00 in the agent/dashboard packages that also import this file
(2.3:1-4.8:1). Root cause: Latte's own accents are calibrated against
Lattes near-white base/crust, not a mid-gray surface1; the Mocha row
does not hit this because Mochas pastel accents are already light, so
they contrast fine against a dark surface1 - Lattes saturated-but-mid
accents do not have the same headroom against Lattes own lighter
surface1.

Fix: darken each accent slot from stock Latte (same hue/saturation,
lower HSL lightness via binary search) until real WCAG contrast against
base03 clears 4.5:1 with margin - verified this also clears >7:1
against base00, so both the swarm-ui chip case and the plain-text case
elsewhere are covered by one set of values. base00-07 stay stock Latte
(no contrast role, just surfaces/foreground). Yellow and peach read
closer to olive/brown than a bright yellow/orange once darkened this
far - an inherent property of darkening a warm hue in sRGB, not a
mapping mistake; boosting saturation further did not rescue it
(checked).

Verified with headless-chromium screenshots against the built swarm-ui
bundle: StatusChip text (the exact case argus flagged) and every chip
tone on /components now read clearly.
2026-08-18 23:29:20 +02:00

9.8 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-derived (light) row gated behind @media (prefers-color-scheme: light) — both use the identical slot mapping, so which row is active never changes what a slot means. The light row's base00-base07 (surfaces/foreground) are stock Latte hexes; base08-base0F (the eight chromatic slots) are darkened from stock Latte to actually clear WCAG AA against how this app uses them (real contrast failures found in review — see the comment above those declarations in colors.css for the numbers and why). 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).