Operant has no theme provider, no runtime CSS-in-JS, and no build-time configuration for
theming. Everything is two HTML attributes plus a layered set of CSS custom properties defined in
css/tokens.css.
data-theme and data-density
Set either attribute on <html> for a whole page, or on any subtree (a single panel, a single
table) to scope it locally:
<html data-theme="dark" data-density="comfortable">
data-theme—"light"or"dark". Both are fully specified; dark is treated as a peer, not an afterthought, since most ops teams live in it.data-density—"comfortable"(default: 40px rows, 14px body) or"compact"(32px rows, 13px body, tighter padding). Components read--row-h,--control-h,--density-pad-x/y,--density-gap, and--density-font, so density cascades automatically — you never set per-component overrides for it.
The token layers
Tokens in tokens.css are layered, in this order, and the layering is enforced by convention, not
by tooling — it's on you (or your coding agent) to respect it:
- Primitives — raw values with no meaning attached:
--slate-500,--blue-600,--iris-400,--amber-700, etc. These exist only to feed the next layer. - Semantic — meaning-mapped tokens components actually consume:
--color-bg,--color-surface,--color-text-muted,--color-primary,--color-danger,--color-agent-active,--color-human-review, and so on. - Theme — the same semantic token names, resolved differently under
:root/[data-theme="light"]versus[data-theme="dark"]. For example--color-bgisvar(--slate-50)in light andvar(--slate-950)in dark — the component never knows or cares which theme is active, it just reads--color-bg. - Density — a small set of layout tokens (
--row-h,--control-h,--density-pad-x,--density-pad-y,--density-gap,--density-font) that resolve differently under[data-density="comfortable"]versus[data-density="compact"].
The rule: never consume a primitive or a raw value
AGENTS.md makes this a hard, non-negotiable rule (§1): components must
never write a raw hex/rgb/hsl color, and must never reference a primitive
(--slate-500, --blue-600, …) directly. They consume semantic tokens only —
--color-*, --space-*, --radius-*, --size-icon-*, --z-*, --w-*, --h-*. Primitives feed
semantic tokens; nothing downstream of that layer should see a primitive name.
If you need a color or spacing value that doesn't have a semantic token yet, the fix is to add
one to tokens.css — in both the light and dark blocks — never to inline a value. This keeps
every component correct in both themes by construction, instead of by remembering to check.
This also explains why AI/agent state colors (--color-agent-active, --color-human-review,
--color-automation-step, …) are separate from status colors (--color-success,
--color-warning, --color-danger, --color-info) even though both are "semantic": they encode
different kinds of meaning (identity/activity vs. outcome) and mixing them would make "the AI is
working" indistinguishable from "something succeeded or failed."
Where to go for the full rules
- Design System — §2 (Token system) has the full token inventory and the rationale for each family.
- Agent Rules — §1 (Tokens) and §10 (Definition of done) are the exact, checkable rules a coding agent (or a reviewer) should enforce before merging any new UI.