78 UI Kit
Components

Utilities

A deliberately small helper layer — the handful of one-liners you would otherwise write a hundred times. This is all of it; the kit is not a utility framework, and anything not listed here does not exist.

Text

Color and size helpers, all reading kit tokens so they follow the theme.

Default body text
_78-text-dim — secondary text
_78-text-sm — 12px
_78-text-xs — 11px
_78-text-accent
_78-text-success
_78-text-warn
_78-text-danger
_78-text-info
_78-fw-600 — semibold
_78-mono — the monospace stack
_78-nums — 1,234,567 tabular figures
text utilities
<div class="site-demo-block">
  <div>Default body text</div>
  <div class="_78-text-dim">_78-text-dim &mdash; secondary text</div>
  <div class="_78-text-sm">_78-text-sm &mdash; 12px</div>
  <div class="_78-text-xs">_78-text-xs &mdash; 11px</div>
  <div class="_78-text-accent">_78-text-accent</div>
  <div class="_78-text-success">_78-text-success</div>
  <div class="_78-text-warn">_78-text-warn</div>
  <div class="_78-text-danger">_78-text-danger</div>
  <div class="_78-text-info">_78-text-info</div>
  <div class="_78-fw-600">_78-fw-600 &mdash; semibold</div>
  <div class="_78-mono">_78-mono &mdash; the monospace stack</div>
  <div class="_78-nums">_78-nums &mdash; 1,234,567 tabular figures</div>
</div>
ClassDeclaration
_78-text-{tone}color: var(--{tone}) — any of the six tones: accent success warn danger info dim
_78-text-smfont-size: var(--fs-sm)
_78-text-xsfont-size: var(--fs-xs)
_78-fw-600font-weight: 600
_78-monofont-family: var(--font-mono)
_78-numsfont-variant-numeric: tabular-nums
_78-nums matters more than it looks: without tabular figures a column of numbers jitters as the digits change. _78-right inside a table already includes it.

Two spellings, one color

Every text tone has a short form too: _78-danger is the same rule as _78-text-danger, for all six tones. The short forms started life on _78-stat-card__num and work anywhere the long spelling does — so neither form is a typo, and you will see both in real code. The tone aliases (green, red, amber, yellow) work in both spellings as well; see Tones.

_78-accent — the same as _78-text-accent
_78-success — the same as _78-text-success
_78-warn — the same as _78-text-warn
_78-danger — the same as _78-text-danger
_78-info — the same as _78-text-info
_78-dim — the same as _78-text-dim
short spellings
<div class="site-demo-block">
  <div class="_78-accent">_78-accent — the same as _78-text-accent</div>
  <div class="_78-success">_78-success — the same as _78-text-success</div>
  <div class="_78-warn">_78-warn — the same as _78-text-warn</div>
  <div class="_78-danger">_78-danger — the same as _78-text-danger</div>
  <div class="_78-info">_78-info — the same as _78-text-info</div>
  <div class="_78-dim">_78-dim — the same as _78-text-dim</div>
</div>

Prefer the _78-text-* spelling in new code — it says that the class colors text, which the short form does not. The short forms exist so that markup written against the stat card keeps working, and so that a habit picked up there is not punished elsewhere.

Changed in 0.3.0
The text tones used to be accent, green, warn, red and dim, so _78-text-danger, _78-text-success and _78-text-info silently did nothing. All six canonical names work now, and the old names still do.

Aurora text

_78-text-aurora runs a slow multi-color gradient through the letters — for a hero word, a role badge, a wordmark. It is one class on any element that holds text, and its default colors are the accent and three category hues, so it re-themes with everything else.

Theme everything, live.

A role badge: Founder · a wordmark: Meridian

_78-text-aurora
<div class="site-demo-block">
  <p style="font-size: 34px; font-weight: 700; line-height: 1.15">
    Theme <span class="_78-text-aurora">everything</span>, live.
  </p>
  <p>
    A role badge: <span class="_78-badge"><span class="_78-text-aurora">Founder</span></span>
    &middot; a wordmark: <strong class="_78-text-aurora" style="font-size: var(--fs-xl)">Meridian</strong>
  </p>
</div>

Your own colors and tempo

--_78-aurora is the color list and --_78-aurora-speed the time for one pass. End the list on the color it starts with and the loop has no seam.

Faster, and greener
custom colors and speed
<strong class="_78-text-aurora" style="font-size: var(--fs-xl); --_78-aurora: var(--success), var(--info), var(--success); --_78-aurora-speed: 3s">
  Faster, and greener
</strong>
PropertyDefaultWhat it does
--_78-aurora--accent-hover, --cat-1, --cat-3, --cat-5, --accent-hoverThe gradient's colors.
--_78-aurora-speed6sOne full pass.
Readable, and still when asked

Every point of the default gradient clears 4.5:1 against --bg, --bg2 and --bg3 in both themes — it uses --accent-hover rather than --accent because that is the higher-contrast end of the accent in both. Check your own colors the same way.

With reduce motion turned on, the gradient stands still. In Windows high-contrast mode, or a browser without background-clip: text, the text is drawn solid — never invisible. Keep it to a word or a short phrase: moving color across a paragraph is hard to read.

Truncate

_78-truncate is the three-property incantation for a single-line ellipsis. It needs a constrained width to do anything — in a flex row, that usually also means min-width: 0 on the item.

A workspace name far too long to fit inside this narrow container
_78-truncate
<div class="_78-truncate">A workspace name far too long to fit…</div>

Layout

Just enough flex to stop you writing the same four lines everywhere. Note that _78-flex also sets align-items: center, which is what you want nine times out of ten.

ClassDeclaration
_78-flexdisplay: flex; align-items: center
_78-flex-between…plus justify-content: space-between
_78-flex-wrapflex-wrap: wrap
_78-flex-coldisplay: flex; flex-direction: column
_78-growflex: 1 1 auto — on a child, to take the slack
_78-gap-8gap: var(--space-2)
_78-gap-12gap: var(--space-3)
_78-gap-16gap: var(--space-4)
Team Northwind 48 seats
_78-flex-between pushed right
layout utilities
<div class="site-demo-block">
  <div class="_78-flex _78-gap-8">
    <span class="_78-badge _78-badge-accent">Team</span>
    <span class="_78-fw-600">Northwind</span>
    <span class="_78-text-dim _78-text-sm _78-grow">48 seats</span>
    <button class="_78-btn _78-btn-sm" type="button">Open</button>
  </div>
  <div class="_78-flex-between">
    <span class="_78-text-dim">_78-flex-between</span>
    <span class="_78-badge _78-badge-success">pushed right</span>
  </div>
</div>

Spacing

Single-axis margins only, on the space scale. There is no padding utility and no left/right margin utility — those are layout decisions that belong in your own CSS.

ClassValueToken
_78-mt-88px top--space-2
_78-mt-1212px top--space-3
_78-mt-1616px top--space-4
_78-mb-44px bottom--space-1
_78-mb-88px bottom--space-2
_78-mb-1212px bottom--space-3
_78-mb-1616px bottom--space-4

The names are the pixel values at default density, but the declarations use the tokens — so a generated theme with a tighter density scales them with everything else.

The escape hatch

The scale stops at 16px, and there are no padding utilities at all. That is deliberate rather than an omission: past 16px, spacing is layout, and a utility layer that keeps going ends up being the framework the kit is not. Above the scale, use the space tokens directly — that is the intended way out.

the space tokens
<!-- above the utility scale, or any padding at all -->
<div style="padding: var(--space-5)">…</div>
<section style="margin-top: var(--space-6)">…</section>

<!-- or in your own stylesheet, which is where anything repeated belongs -->
<style>.report-head { padding: var(--space-5) var(--space-4) }</style>

The tokens run --space-1 through --space-6 (4 · 8 · 12 · 16 · 24 · 32px) and scale with the theme's density, which a hard-coded 24px would not. They are listed with every other token on the Theming page.

Lists

The reset strips list padding everywhere, on purpose. Inside the components that hold prose (_78-alert-body, _78-card, _78-modal-body and _78-details-body) a <ul> or <ol> with no class of its own gets an indent and its markers back. Everywhere else, lists stay bare. The rule is written with :where(), so any rule of yours out-ranks it.

_78-list-plain takes the markers and the indent off, anywhere.

A plain list in a card
  • Indented, markers inside the padding
  • A little space between items
_78-list-plain
  • No markers
  • No indent
  • Still a list to a screen reader
lists
<div class="_78-card-grid">
  <div class="_78-card">
    <div class="_78-card-title">A plain list in a card</div>
    <ul>
      <li>Indented, markers inside the padding</li>
      <li>A little space between items</li>
    </ul>
  </div>
  <div class="_78-card">
    <div class="_78-card-title">_78-list-plain</div>
    <ul class="_78-list-plain">
      <li>No markers</li>
      <li>No indent</li>
      <li>Still a list to a screen reader</li>
    </ul>
  </div>
</div>

JS helpers

Two small functions on _78.util, with no dependencies and no DOM. They are the two pieces of ad-hoc code every dashboard ends up writing. New in 0.3.0

sortAlpha(a, b)

A comparator for human sorting: case-insensitive, locale-aware, and numbers compared as numbers, so “Item 2” comes before “Item 10”. Blanks (null, undefined, "") sort last. Pass it straight to array.sort(), or use it as a Tabulator column's sorter.

Plain sort():
With sortAlpha:
sortAlpha
['Item 10', 'item 2', 'Item 1'].sort(_78.util.sortAlpha);
// → ['Item 1', 'item 2', 'Item 10']

new Tabulator('#accounts', {
  columns: [{ title: 'Customer', field: 'customer', sorter: _78.util.sortAlpha }]
});

duration(ms, options)

Milliseconds as plain English. The last part shown is rounded, with carry, and a part that rounds to zero is left out — so it never prints “0 min”. Days are the largest unit; anything that isn't a number comes back as an empty string.

OptionDefaultWhat it does
longfalsetrue spells the units out: “2 hours 5 minutes” instead of “2 hrs 5 min”.
parts2The most units to show. 1 gives “3 days”, 3 gives “3 days 4 hrs 50 min”.

Rendered live by the kit this page loads:

msduration(ms){ long: true }{ parts: 1 }
duration
_78.util.duration(45000);                      // '45 sec'
_78.util.duration(7500000);                    // '2 hrs 5 min'
_78.util.duration(7500000, { long: true });    // '2 hours 5 minutes'
_78.util.duration(276600000, { parts: 3 });    // '3 days 4 hrs 50 min'
_78.util.duration(-180000);                    // '-3 min'

Screen-reader only

_78-sr-only hides content visually while leaving it available to assistive technology. It is the correct tool for a table <caption> you do not want to show, for extra context inside a button, and for giving an icon-only control a name where aria-label will not do.

_78-sr-only
<button class="_78-btn" type="button">
  Export
  <span class="_78-sr-only">— downloads a CSV of the current view</span>
</button>
Not the same as hidden
display: none and the hidden attribute remove content from everyone, screen readers included. Use _78-sr-only when the information should still be announced.