OperantAI Workflow Design System
ComfortableCompact

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:

  1. Primitives — raw values with no meaning attached: --slate-500, --blue-600, --iris-400, --amber-700, etc. These exist only to feed the next layer.
  2. 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.
  3. Theme — the same semantic token names, resolved differently under :root/[data-theme="light"] versus [data-theme="dark"]. For example --color-bg is var(--slate-50) in light and var(--slate-950) in dark — the component never knows or cares which theme is active, it just reads --color-bg.
  4. 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.