Skip to content

Theming the kit

The ui-* class names are the contract; static/ui.css is one implementation of it; the --ui-* tokens are the retune surface. There are three tiers, smallest change to largest — reach for the first that fits.

Tier 1 — retune the tokens

Override --ui-* on :root, or on any container (tokens cascade, so a wrapper themes just its subtree). One token can carry a whole dimension of the design.

.acme-theme {
  /* brand — set the whole primary ramp */
  --ui-primary: #e2540c; --ui-primary-hover: #cf4a26; --ui-primary-active: #b8411f;

  /* density + geometry */
  --ui-border-w: 1px;              /* the kit-wide border weight */
  --ui-radius: 10px;               /* control corners */
  --ui-radius-pill: var(--ui-radius);   /* square the pills (badges / switch / progress) */
  --ui-space-card: 16px 20px;      /* card head / foot padding */

  /* colors that used to be hard-coded */
  --ui-on-accent: #fff;            /* text / thumb on a filled accent */
  --ui-tooltip-bg: #111827;
  --ui-backdrop: rgba(0, 0, 0, .5);  /* modal / drawer / palette scrim */
}

Dark mode. The dual-valued tokens are light-dark(<light>, <dark>), and :root sets color-scheme: light dark — so dark follows the OS by default, and a .ui-theme-dark / .ui-theme-light on any ancestor forces it (it sets color-scheme, which the tokens resolve against). Retune both schemes at once by re-declaring a token with light-dark().

<body class="ui-root ui-theme-dark"></body>   <!-- forces the whole kit dark, OS-independent -->

Tier 2 / 3 — reskin over the class contract

For a full design-system swap (Tailwind / Flowbite / Bootstrap), re-implement the ui-* class bodies against the same names — with Tailwind @apply or plain CSS — and load your sheet instead of ui.css.

This is the path that also reaches the runtime surface: the state classes widgets.js toggles (.ui-active, .is-checked, .ui-th-asc) and the ::after / ::before content hooks (sort carets, the combobox check, the switch knob) appear in no template, so a reskin that styles only the base classes ships visibly broken widgets. Style the runtime selectors too.

.ui-btn                       { @apply inline-flex items-center gap-2 h-8 px-3 rounded-md; }
.ui-btn--primary              { @apply bg-brand-600 text-white hover:bg-brand-700; }
.ui-tab.ui-active             { @apply border-brand-600 text-brand-700; }   /* widgets.js toggles .ui-active */
.ui-combobox__opt.is-active   { @apply bg-brand-50; }                       /* and .is-active */
.ui-th-asc::after             { content: "▲"; }                            /* keep the sort-caret hook */

Verify coverage with the contract gate — it derives the required selectors from ui.css + widgets.js (no hand-maintained list) and reports what your sheet misses:

$ uv run python -m situ_ui.check_css app/static/acme.css
uncovered ::after/::before content hooks (carets / check / knob  invisible in templates): .ui-th-asc::after 

Wire it into your own lint loop so a reskin that falls behind the kit fails the gate, not the browser.

Eject — own the component

When a component's DOM structure must change (not just its classes — a Flowbite card with a differently-nested header/body), copy its file-pair and own it. This is a different path through the same splice/compile pipeline, so a fork behaves exactly like a kit component.

$ uv run python -m situ_ui.eject Card --to app/ui     # -> app/ui/card.html + app/ui/card.py
UI = situ_ui.kit().with_override("Card", Path("app/ui/card"))   # your fork wins on the <Card/> tag
mount_tree(path="/app", root=Path("app/dashboard"), components=UI, template="page.html.j2", meta=META)

Point situ check at your fork directory so a stale or broken fork fails at the gate. Eject the presentational components (card, dialog shell, field, page header, stat, empty) where a fork is cheap; leave the widgets.js-driven ones on the class contract, where a fork gains little and risks the JS contract (widgets.js querySelectors their ui-* names).

The design rationale — why the class contract, not a compile-time class map — is in notes/32-kit-theming.md.