# 78 UI Kit — complete reference > A theme-forward, no-build UI kit in plain HTML / CSS / JS. One stylesheet, one script, no dependencies. > Every color resolves at runtime from CSS custom properties, so re-theming the components — *and* your > Tabulator tables, FullCalendar calendars and Chart.js charts — is an override block, not a rebuild. > MIT © 2026 James Robinson. This file is the complete, copy-paste-ready reference: install, theming, every component with all of its classes, variants, `data-*` attributes and JS options, the library adapters, and the theme generator API. For a short index, read `llms.txt`. For the live gallery, see https://78uikit.com. Version 0.x — the public API may still change before 1.0. --- ## Contents 1. Install 2. Theming — tokens, the mechanism, the theme toggle 3. Layout — the app shell and the contents rail 4. Buttons and the segmented control 5. Cards, stat / KPI cards, grids and the meta line 6. Forms and the toggle switch 7. Badges, tags, pills and the status line 8. Table 9. Tabs 10. Dropdown menu 11. Notifications — modal, toast, inline alert 12. Data-viz primitives — `_78.viz`, figure rows and split bars 13. Disclosure — `._78-details` 14. Utilities — and the JS helpers, `_78.util` 15. Library adapters — Tabulator, FullCalendar, Chart.js 16. Theme generator — `_78.gen` 17. Events, storage keys and file map --- ## 1. Install One stylesheet, one script, and an inline snippet that sets the theme before first paint. ```html … ``` **Distribution.** `@v0.4.0` pins a release; `@main` tracks the latest and moves on every push. Minified builds are served as `css/kit.min.css` and `js/_78.min.js`. Self-hosting is the same files at your own path — the kit contains no absolute URLs. **What `kit.css` pulls in.** `tokens.css`, `reset.css`, and every component stylesheet. Library adapters are deliberately *not* imported: include only the ones you use. **What `_78.js` does on load.** It wires every `._78-theme-toggle`, mounts every `._78-tabs`, mounts every `._78-menu-wrap`, wires the app shell (`._78-nav-toggle`, `._78-nav-collapse`, `._78-scrim`), runs `_78.viz.mountAll()` over the document, and delegates a click handler for `._78-alert-close`. It is safe to load with `defer`, and all mounting is idempotent — calling a `mount` function twice does nothing. **Markup added later.** After injecting HTML, re-mount just that subtree: ```js _78.viz.mountAll(container); // sparklines, progress, bars, donuts, trends _78.tabs.mountAll(container); // tab lists _78.shell.mountMenus(container); // dropdown menus ``` **The reset.** `reset.css` is minimal: `box-sizing: border-box`, zeroed margins, `body` painted with `--bg` / `--text` / `--font`, block-level media, unstyled `button`, links in `--accent`, themed thin scrollbars, a `:focus-visible` ring in `--accent`, and a `prefers-reduced-motion` block that neutralizes animation. It styles `body`, so it is the one part of the kit that is not purely opt-in. --- ## 2. Theming ### 2.1 The rule A theme is **only color**. Color tokens live in the `[data-theme]` blocks; everything else — radius, type scale, spacing, control heights, layout widths, transitions — lives in a plain `:root` shared by every theme. Nothing in the kit hardcodes a color; every component reads tokens. That is the entire theming mechanism, and it is the only one: never fork `tokens.css`, override it. ### 2.2 The mechanism ```css :root { /* non-color tokens, shared by every theme */ } :root, :root[data-theme="dark"] { color-scheme: dark; /* color tokens */ } :root[data-theme="light"] { color-scheme: light; /* color tokens */ } ``` Bare `:root` carries the dark palette too, so the kit still looks right if JS never runs. `color-scheme` is set per theme, so native scrollbars, form controls and the browser's own UI follow along. The pre-paint snippet reads `localStorage['_78-theme']` (`"dark"` / `"light"` / `"system"`), resolves `"system"` through `matchMedia('(prefers-color-scheme: light)')`, and writes two attributes on ``: - `data-theme` — the **resolved** theme, `"dark"` or `"light"`. This is what the CSS keys off. - `data-theme-pref` — the **preference**, `"dark"` / `"light"` / `"system"`. This is what the toggle icon keys off. ### 2.3 Overriding tokens Put overrides in a stylesheet or ` ``` Tokens cascade on any element, not just `:root`, so scoping one to a subtree re-themes just that subtree: ```html
…
``` ### 2.4 The `-lo` tints Every accent and semantic color ships a `-lo` companion: the same hue at low alpha, used as a **background** under solid text in the full-strength color. This is why the kit never tints with `opacity` — `opacity` washes out the label along with the fill. ```css ._78-badge-success { background: var(--success-lo); color: var(--success); } ``` ### 2.5 Full token list #### Core color — per theme | Token | What it is | |---|---| | `--bg` | App background, the furthest-back surface | | `--bg2` | Raised surface: cards, topbar, sidebar, modal, menu, toast | | `--bg3` | Inset surface: inputs, badges, modal footer, empty states | | `--border` | Every 1px edge, and the track behind progress / donut fills | | `--text` | Primary text | | `--dim` | Secondary text, icons, table headings, placeholders | | `--accent` | The project's own color — the main per-project override | #### Accent derived — per theme | Token | What it is | |---|---| | `--accent-hover` | The accent's hover state (lighter on dark, darker on light) | | `--accent-text` | Text drawn *on* an accent fill; must pass contrast against `--accent` | | `--accent-lo` | Low-alpha accent tint, for fills behind accent-colored text | | `--accent-lo-hover` | The tint's hover state | | `--accent-glow` | The `box-shadow` on a primary button | #### Semantic — per theme `--success` · `--warn` · `--danger` · `--info`, each with a matching `-lo` tint (`--success-lo`, `--warn-lo`, `--danger-lo`, `--info-lo`). Semantic hues stay conventional — green / amber / red / blue — and deliberately never follow the accent. A "themed red that isn't red" is a usability trap. #### Categories — per theme `--cat-1` … `--cat-6`, each with a `-lo` tint (`--cat-1-lo` …). Six label colors for things that are **not states** — a source, a team, a type. Their hues (violet, teal, rose, olive, orchid, bronze) are kept clear of the semantic hues so a category never reads as success / warn / danger, and like the semantics they don't follow the accent. Each clears 4.5:1 as badge text on its own tint in both themes. Use them in order: `cat-6` (bronze) is the one nearest a state (warn), so a short list never reaches it. #### Charts — per theme `--series-2` — the second series in a chart: a true gray that recedes behind `--accent` and, having no hue, can never sit near it in hue whatever the accent is. It clears 3:1 on `--bg2` in both themes. The Chart.js adapter uses it for the second dataset (§15.3); past two series it continues with `--cat-*`. #### Surfaces, rows, focus — per theme | Token | What it is | |---|---| | `--row-hover` | Table / menu / list row hover wash | | `--row-border` | The lighter rule between table rows (vs. `--border` for the frame) | | `--row-selected` | Selected row background; defaults to `var(--accent-lo)` | | `--ring` | Focus ring color for form controls, drawn at `--ring-w` thickness | | `--overlay` | The modal backdrop | | `--shadow` | Resting elevation (cards) | | `--shadow-lg` | Raised elevation (modal, toast, hovered card) | #### Non-color — shared `:root`, one set for every theme | Group | Tokens | Shipped values | |---|---|---| | Type | `--font`, `--font-mono` | Inter + system stack; `ui-monospace` stack | | Type scale | `--fs-xs` `--fs-sm` `--fs-base` `--fs-lg` `--fs-xl`, `--lh` | 11 / 12 / 14 / 16 / 20px, 1.5 | | Spacing | `--space-1` … `--space-6` | 4 / 8 / 12 / 16 / 24 / 32px | | Radii | `--radius-sm` `--radius` `--radius-lg` `--radius-pill` | 6 / 8 / 10 / 999px | | Controls | `--control-h` `--control-h-sm` `--icon-btn-size` `--ring-w` | 36 / 28 / 32 / 3px | | Layout | `--sidebar-w` `--rail-w` `--header-h` `--content-max` | 240 / 60 / 56 / 1120px | | Motion | `--transition` `--transition-slow` | 140ms ease / 240ms ease | ### 2.6 `_78.theme` API ```js _78.theme.pref // getter → 'dark' | 'light' | 'system' _78.theme.current // getter → the resolved 'dark' | 'light' _78.theme.set('light') // set + persist + apply; returns the resolved theme _78.theme.cycle() // Dark → Light → System → Dark; returns the new preference _78.theme.apply('dark') // apply without persisting; returns the resolved theme _78.theme.mount(el) // wire an element as a toggle (idempotent) _78.theme.KEY // '_78-theme' — the localStorage key _78.theme.PREPAINT // the pre-paint snippet, as a string (for docs / tooling) ``` Free behavior: while the preference is `"system"` the page live-follows the OS setting, and a theme change made in another tab is picked up through the `storage` event. ### 2.7 The `_78:themechange` event Fires on `document` after every change, including the initial apply on load. ```js document.addEventListener('_78:themechange', function (e) { console.log(e.detail.theme); // 'dark' | 'light' — resolved console.log(e.detail.pref); // 'dark' | 'light' | 'system' myCanvasThing.redraw(); // the hook for anything that can't read CSS variables }); ``` Anything drawn to a `` needs this event; anything drawn in CSS or SVG re-themes by itself. ### 2.8 The theme toggle Any element carrying `._78-theme-toggle` is wired on load — no JS to write. Give it the three icons and the CSS shows the one matching the current *preference*: ```html ``` `title` and `aria-label` are set and kept current by the kit. A toggle with no icons works too — it is simply a button that cycles the theme. | Class | Role | |---|---| | `._78-theme-toggle` | Marks an element as a toggle; auto-wired on load | | `._78-theme-icon` | An icon inside it; `data-pref="dark"` / `"light"` / `"system"` picks when it shows | ### 2.9 Tones — one vocabulary for every component **This is the single list.** Every component that takes a tone takes the same names, and they are the token names: | Canonical | Token | Aliases (permanent) | Means | |---|---|---|---| | `accent` | `--accent` | | Your brand color; a neutral highlight | | `success` | `--success` | `green` | Done, healthy, up-is-good | | `warn` | `--warn` | `amber`, `yellow` | Needs attention, not broken | | `danger` | `--danger` | `red` | Failed, destructive, down-is-bad | | `info` | `--info` | | Neutral information | | `dim` | `--dim` | | De-emphasized; the plain look, stated | Write the canonical name. The aliases exist so older markup keeps working and so a guess doesn't fail silently; they are selector lists on the same rules, and they will not be removed. | Component | Class or option | All six? | |---|---|---| | Badge | `._78-badge-{tone}` | yes | | Tag | `._78-tag-{tone}` | yes | | Text | `._78-text-{tone}`, or the short `._78-{tone}` | yes | | Stat-card number | `._78-stat-card__num ._78-{tone}` | yes | | Inline alert | `._78-alert-{tone}` | yes; `dim` is the plain alert | | Data-viz (sparkline, progress, bar row, donut, figure, split bar) | `._78-tone-{tone}`, `data-tone` / `tone:` | yes | | Modal | `._78-modal-{tone}`, `_78.modal.open({ tone })` | yes; `dim` is the plain modal | | Toast | `_78.notify(msg, { type })`, `._78-toast-{tone}` | yes; `danger` is the same as the toast's own `error` | In JS, `_78.tone(name)` returns the canonical name for any tone or alias, or `null`: ```js _78.tone('red') // → 'danger' _78.tone('Amber') // → 'warn' _78.tone('brand') // → null _78.tone.names // → ['accent', 'success', 'warn', 'danger', 'info', 'dim'] _78.tone.aliases // → { green: 'success', red: 'danger', amber: 'warn', yellow: 'warn' } ``` **Categories** are a second, separate set for labels that are not states: `cat-1` … `cat-6`, on badge (`._78-badge-cat-2`), tag, text (`._78-text-cat-2` / `._78-cat-2`) and viz tone (`._78-tone-cat-2`, `data-tone="cat-2"`). Reach for a category when a status tone would lie — a trade source called "Scout" shown in `warn` reads as a caution. `_78.tone('cat-2')` → `'cat-2'`; `_78.tone.categories` lists them. Alerts, modals and toasts are about states, so they don't take categories. ```html AI You Watch list ``` Not tones, on purpose: button variants (`._78-btn-primary` / `-ghost` / `-danger` are actions, not states), the score pill's `._78-high` / `-mid` / `-low` bands, and `._78-seg-accent` (a style for the selected segment). *(Added in 0.3.0. Before it, "danger" was `-red` on a badge and text but `-danger` on an alert; both spellings now work on every component.)* --- ## 3. Layout — the app shell and the contents rail A fixed topbar, an optional sidebar that collapses to an icon rail on desktop and becomes an off-canvas drawer on mobile, and a main region that makes its own room for both. Because the topbar and sidebar are `position: fixed` and nothing here styles ``, **existing page content never has to be re-wrapped** — add `._78-app-main` to your `
` and you are done. ### 3.1 Full markup ```html
Your app Production
…your page…
``` A topbar-only page is the same markup minus the `