78 UI Kit
Theme generator

One accent in, a whole theme out

Pick a color. The generator derives the complete token set for light and dark in OKLCH, checks every contrast pair that matters, nudges anything that would fail, and hands you CSS you can paste after kit.css.

Theme
How the grays lean. Subtle, and it changes the whole feel.
0 for square, 20 for very round. Drives every corner in the kit.
Scales the spacing and control heights, not the type.
Contrast
—
Light #0073ea
Dark #4b9eff
Each theme fits your accent to its own background

The two chips above are not the color you picked, and that is deliberate — dark is not inverted light. Your hue is kept, but its lightness is retargeted per theme (darker for light, lighter for dark) and its chroma eased back on dark, then nudged to the nearest value that clears both checks at once: 3:1 against the card surface so the accent is visible, and 4.5:1 for text sitting on an accent fill. Those two pull in opposite directions, which is why one fixed color cannot serve both themes.

Paste-ready CSS

Drop this into a <style> or your own stylesheet after kit.css. Components, adapters and the data-viz primitives all follow it — there is nothing else to change.

…
Reference

How it works

Four inputs, a share URL, and a small API if you would rather drive it yourself.

Options

The four config keys are the whole input surface. Everything else — every color token for both themes — is derived from them.

KeyValuesWhat it does
accent any hex Your brand color. Everything in the accent family is derived from it, separately for each theme. Default #0073ea.
neutral neutral · cool · warm · accent The hue the grays lean toward. neutral is a pure gray; cool and warm add a slight blue or yellow cast; accent tints them with your own hue.
radius 0–20 Corner radius in pixels. Sets --radius and derives --radius-sm and --radius-lg from it.
density compact · cozy · comfortable Scales the space scale and control heights — 0.75×, 1× or 1.25×. Type sizes are not touched.
Dark is not inverted light
Each theme gets its own lightness and chroma targets, so a color that works on a dark surface is derived for that surface rather than flipped. The semantic colors — green, amber, red, blue — never follow the accent; a themed “red” that is not red is a usability trap.

The ?theme= URL

Every change rewrites the address bar, so the URL is always a link to exactly what you are looking at. The format is four dot-separated fields — short enough to read, and no backend needed.

the share format
?theme=accent.neutral.radius.density

?theme=7c5cff.cool.10.cozy      →  violet accent, cool grays, 10px radius, cozy
?theme=0d9488.warm.0.compact    →  teal accent, warm grays, square corners, compact

The accent is written without its #, and a hand-written ?theme=7c5cff works — but mind the trailing fields, because they do not all fall back the same way.

FieldIf missing or invalid
accentFalls back to #0073ea.
neutralFalls back to neutral.
radiusFalls back to 0, not to the default 8 — so ?theme=7c5cff gives you square corners.
densityFalls back to cozy.

Write all four fields when you are sharing a link on purpose. The buttons above always do — encode() emits the complete string.

On load this page reads ?theme= first, then a theme you previously applied, then the kit default — so a shared link always shows its own theme rather than the reader's saved one.

The _78.gen API

The generator is a normal part of the kit, in js/generator.js. Load it alongside _78.js and you can derive themes in your own product — a per-workspace accent, a customer-branded portal.

loading the generator
<script defer src="https://cdn.jsdelivr.net/gh/1978io/78UIKit@v0.4.0/js/_78.js"></script>
<script defer src="https://cdn.jsdelivr.net/gh/1978io/78UIKit@v0.4.0/js/generator.js"></script>
_78.gen
// Derive — config in, both token sets out
var result = _78.gen.derive({
  accent: '#7c5cff', neutral: 'cool', radius: 10, density: 'cozy'
});
// result = { config, accent, light, dark, base, notes }

// Output
_78.gen.css(result);                 // the full paste-ready stylesheet
_78.gen.css(result, { base: false }); // colors only, no radius/density block

// Apply to the live page, and persist it
_78.gen.apply(result);   // writes a <style> and saves config + css to localStorage
_78.gen.clear();         // remove it and go back to the kit default
_78.gen.stored();        // the saved config, or null

// Contrast
_78.gen.report(result.light, 'light');
// [{ label, ratio, need, large, soft, pass }, …]

// Sharing
_78.gen.encode(result.config);        // '7c5cff.cool.10.cozy'
_78.gen.decode('7c5cff.cool.10.cozy'); // → a config object
_78.gen.fromUrl();                    // read ?theme= from the current URL
_78.gen.shareUrl(result.config);      // the full URL with ?theme= set

// Preview one theme on a container, without touching the page
_78.gen.preview(el, result.light, result.base);
MemberWhat it does
derive(config)The whole job. Returns { config, accent, light, dark, base, notes }; notes lists any lightness nudged to pass contrast.
css(result, opts)Paste-ready stylesheet. { base: false } omits the shape and density block.
block(sel, tokens, grouped)One CSS rule, if you want to assemble output yourself.
apply(result)Applies as a real stylesheet — both theme blocks, so the theme toggle keeps working — and saves it. Fires _78:genchange.
clear()Removes the stylesheet and the saved theme.
stored()The saved config, or null.
preview(el, tokens, base)Writes tokens onto one element as inline custom properties — how the two panes above show both themes at once.
report(tokens, mode)The contrast rows for a derived theme.
encode · decode · fromUrl · shareUrlThe ?theme= round trip.
DEFAULTS · STORAGEThe default config, and the localStorage key (_78-gen).
PREPAINTThe pre-paint snippet, as a string — see below.

Restoring a saved theme before first paint

apply() stores the generated CSS next to the config, so it can be put back with no re-derivation. This snippet goes after the theme snippet and before any stylesheet — it is what stops an applied theme flashing the default on every navigation. This site runs it on every page, which is why Apply above survives a reload.

the generated-theme pre-paint snippet
<script>
(function(){try{var s=JSON.parse(localStorage.getItem('_78-gen')||'null');if(!s||!s.css)return;
var e=document.createElement('style');e.id='_78-gen-style';e.textContent=s.css;
document.documentElement.appendChild(e);document.documentElement.dataset._78Gen='on';}catch(e){}})();
</script>

See Theming for the token contract the generator writes to, and Getting started for the theme snippet that goes beside this one.