78 UI Kit
Guides

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 :root and are shared by both themes. Switching theme never moves your layout.
  • Every accent and semantic color has a -lo tint. So you stop reaching for opacity to make a soft background and washing out the text sitting on top of it.
  • color-scheme is 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.

TokenWhat it is
--bgThe page background
--bg2Raised surfaces — cards, the topbar, the sidebar
--bg3Inset surfaces — inputs, wells, tracks
--borderEvery hairline and divider
--textPrimary text
--dimSecondary text, labels, captions
--accentYour 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.

GroupTokens
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
Semantic colors stay conventional
Green, amber and red never follow your accent. A themed “red” that is not red is a usability trap — people read danger by color before they read the label.

Non-color

Shared by both themes, in a plain :root. Change these to restyle rather than recolor.

GroupTokens
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.

how the kit defines a theme
: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.

the pre-paint snippet
<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.

redrawing a canvas
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.

The accent's tints are their own tokens. --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.
your-theme.css
/* 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.

Or let the generator write it for you

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.

Open the theme generator