Theming
Most component libraries make color the hard thing to change. 78 UI Kit resolves color at runtime with CSS custom properties, so re-theming is overriding the tokens you want in a stylesheet of your own — no build, no tooling, no rebuild step.
Three deliberate choices
-
Structure is separate from color. A theme is only color. Non-color
tokens — radius, fonts, spacing, layout sizes — live in a plain
:rootand are shared by both themes. Switching theme never moves your layout. -
Every accent and semantic color has a
-lotint. So you stop reaching foropacityto make a soft background and washing out the text sitting on top of it. -
color-schemeis set per theme, so native scrollbars, form controls and date pickers follow along instead of staying stubbornly light.
Tokens
Token names are the contract. The values the kit ships are just a neutral default palette, and they are meant to be overridden — that is the whole design. If you set the names below, every component follows.
Core color
The seven that do most of the work. Each is defined once for light and once for dark.
| Token | What it is |
|---|---|
--bg | The page background |
--bg2 | Raised surfaces — cards, the topbar, the sidebar |
--bg3 | Inset surfaces — inputs, wells, tracks |
--border | Every hairline and divider |
--text | Primary text |
--dim | Secondary text, labels, captions |
--accent | Your brand color — the one people actually change |
Derived color
The kit ships these too, so you get a complete theme without deriving anything by hand. Set them when you want precise control — or let the theme generator work them out from your accent.
| Group | Tokens |
|---|---|
| Accent family | --accent-lo · --accent-lo-hover · --accent-hover ·
--accent-text (text on an accent fill) · --accent-glow |
| Semantic | --success · --warn · --danger ·
--info, each with a matching -lo tint |
| Categories | --cat-1 … --cat-6, each with a matching -lo tint
— label colors that are not states (Tones) |
| Charts | --series-2 — the second chart series, a gray that recedes behind the accent
(Chart.js adapter) |
| Rows | --row-hover · --row-border · --row-selected |
| Chrome | --ring (focus) · --overlay (modal backdrop) ·
--shadow · --shadow-lg |
Non-color
Shared by both themes, in a plain :root. Change these to restyle rather than recolor.
| Group | Tokens |
|---|---|
| Shape | --radius-sm · --radius · --radius-lg · --radius-pill |
| Type | --font · --font-mono · --fs-xs through --fs-xl · --lh |
| Controls | --control-h · --control-h-sm · --icon-btn-size · --ring-w |
| Layout | --sidebar-w · --rail-w · --header-h · --content-max |
| Space | --space-1 through --space-6 (4 · 8 · 12 · 16 · 24 · 32px) |
| Motion | --transition · --transition-slow |
The mechanism
Colors live in two blocks on the root element, selected by a data-theme attribute.
Each block also sets color-scheme, which is what brings the native controls along.
:root[data-theme="dark"] {
color-scheme: dark;
--bg: #0f1117;
--text: #e8eaf0;
--accent: #4b9eff;
/* …and the rest */
}
:root[data-theme="light"] {
color-scheme: light;
--bg: #f2f2f2;
--text: #202020;
--accent: #0073ea;
}
Setting it before the first paint
Something has to put that attribute on <html>, and it has to happen before the
browser paints — otherwise a returning visitor sees a flash of the wrong theme. That is what
the inline snippet from
Getting started is for. It reads the saved
preference, resolves system against prefers-color-scheme, and sets both
attributes before any stylesheet loads.
<script>
(function(){var p=localStorage.getItem('_78-theme')||'system';
var t=p==='system'?(matchMedia('(prefers-color-scheme: light)').matches?'light':'dark'):p;
var r=document.documentElement;r.dataset.theme=t;r.dataset.themePref=p;})();
</script>
Two attributes, because they answer different questions.
data-theme is the resolved theme and drives the CSS;
data-theme-pref is what the visitor actually chose, including
system, and is what the toggle's icon follows.
Reacting to a switch
Every change fires a _78:themechange event on document. CSS updates
itself, so you only need this for things CSS cannot reach — a canvas chart, a map, an
embedded editor.
document.addEventListener('_78:themechange', function (e) {
// e.detail = { theme: 'dark' | 'light', pref: 'dark' | 'light' | 'system' }
myChart.update();
});
The Chart.js adapter already does exactly this, which is why themed charts keep matching after a switch instead of only on first load.
Re-theme it yourself
Put your overrides after the kit so they win, and set only the tokens you care about — anything you leave alone keeps the shipped default.
--accent-hover, --accent-lo, --accent-lo-hover,
--accent-glow and --ring each hold a literal color, not an expression
derived from --accent. Setting --accent by itself recolors the solid
fills and leaves every hover, tint, glow and focus ring on the old color — so set them
alongside it, as below, or let the generator do it.
/* after kit.css */
:root[data-theme="dark"] {
--bg: #101418;
--bg2: #161b21;
--bg3: #1d242c;
--border: #263039;
--text: #e6edf3;
--dim: #8b98a5;
--accent: #7c5cff;
--accent-text: #0b0d10;
--accent-hover: #9b80ff;
--accent-lo: rgba(124, 92, 255, .16);
--accent-lo-hover: rgba(124, 92, 255, .26);
--accent-glow: 0 2px 10px rgba(124, 92, 255, .30);
--ring: rgba(124, 92, 255, .35);
}
:root[data-theme="light"] {
--bg: #f6f7f9;
--bg2: #ffffff;
--border: #e2e6ea;
--text: #1a1f24;
--accent: #6544e0;
--accent-text: #ffffff;
--accent-hover: #5436c4;
--accent-lo: rgba(101, 68, 224, .10);
--accent-lo-hover: rgba(101, 68, 224, .18);
--accent-glow: 0 2px 8px rgba(101, 68, 224, .25);
--ring: rgba(101, 68, 224, .25);
}
That is the entire mechanism. No Sass variables, no rebuild, no cascade archaeology — the components read the tokens at runtime, so they change the moment the values do. What it asks of you is the bookkeeping: a theme is a set of related colors, and it is on you to keep them related. Which is the argument for the easy path.
The theme generator takes one accent and derives the whole token set for light and dark in OKLCH — tints, glow and focus ring included — checks every contrast pair that matters, nudges anything that would fail, and hands you a paste-ready block like the one above. For most themes this is the shortest honest route: one color in, a complete contrast-checked stylesheet out.
Which is why --accent usually ends up slightly different in each theme
rather than one shared color: the hue is kept, but the lightness is retargeted per theme —
darker on light, lighter on dark — and then fitted so it clears 3:1 against the card surface
and 4.5:1 for text on an accent fill. One fixed color cannot satisfy both backgrounds, so
the generator does not pretend otherwise. It reports every value it moved.