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().
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.
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.