78 UI Kit
Components

Cards

A surface with a border and padding, and the KPI variant built on top of it. Two grids lay them out so a dashboard needs no layout CSS of its own.

Card

_78-card is the surface; _78-card-title is the heading slot. Consecutive cards space themselves, so a stack of them needs no margins (in a grid, the gap does it instead).

Monthly summary

Revenue held steady while support volume fell for the third month running.

card
<div class="_78-card">
  <div class="_78-card-title">Monthly summary</div>
  <p>Revenue held steady while support volume fell for the third month running.</p>
</div>

Head and foot

_78-card-head is a flex row for a title with a control beside it — it clears the title's bottom margin for you. _78-card-foot is the closing row at the bottom, with a top border above it.

The foot is full-bleed: it cancels the card's padding with negative margins and re-applies its own, so its border reaches both edges and its bottom sits flush with the card — exactly like the foot of a modal. _78-card-foot-fill adds the modal foot's tinted --bg3 surface to it. A short form fits in the foot too — see the inline form.

Team members

Twelve people have access to this workspace.

Export

A CSV of everything in the current view, including hidden columns.

Last export was 4 days ago
card head and foot
<div class="site-demo-block">
  <div class="_78-card">
    <div class="_78-card-head">
      <div class="_78-card-title">Team members</div>
      <button class="_78-btn _78-btn-sm" type="button">Invite</button>
    </div>
    <p>Twelve people have access to this workspace.</p>
    <div class="_78-card-foot">
      <button class="_78-btn _78-btn-ghost _78-btn-sm" type="button">Manage roles</button>
      <button class="_78-btn _78-btn-primary _78-btn-sm" type="button">Done</button>
    </div>
  </div>

  <div class="_78-card">
    <div class="_78-card-title">Export</div>
    <p>A CSV of everything in the current view, including hidden columns.</p>
    <div class="_78-card-foot _78-card-foot-fill _78-flex _78-flex-between">
      <span class="_78-text-sm _78-text-dim">Last export was 4 days ago</span>
      <button class="_78-btn _78-btn-primary _78-btn-sm" type="button">Export CSV</button>
    </div>
  </div>
</div>
Changed in 0.2.0
The foot used to sit inset, with its border floating short of both edges. It is now full-bleed. If you had written your own negative margins to get that effect, delete them — they will now pull the row outside the card.

The foot is a plain block, not a flex row: buttons inside it sit at the left in source order. Reach for _78-flex, _78-flex-between or _78-grow when you want them arranged, as the second card above does.

Clickable cards

_78-card-link turns a card into a link target: it lifts on hover and takes the accent border. Put it on an <a> so it is keyboard reachable and announced as a link — a div with a click handler is neither. The Overview page is a grid of these.

clickable card
<a class="_78-card _78-card-link" href="#card">
  <div class="_78-card-title">Billing</div>
  <p class="_78-text-sm _78-text-dim" style="margin:0">Plans, invoices and payment methods.</p>
</a>

KPI stat card

_78-stat-card is the dashboard card: a label, a big number, a delta and a sparkline. Every part is optional — a label and a number alone is a perfectly good stat card.

ElementWhat it is
_78-stat-card__headFlex row holding the label and an optional icon or control slot.
_78-stat-card__labelThe small uppercase caption.
_78-stat-card__iconA tinted square for a small SVG.
_78-stat-card__actionsA control slot at the end of the head — a small toggle beside the label. See below.
_78-stat-card__numThe big number.
_78-stat-card__rowFlex row for the delta and its caption.
_78-stat-card__subThe caption beside or under the number.
_78-stat-card__sparkSparkline slot — give it data-values.
_78-stat-card__linesKey : value lines. See below.
_78-stat-deltaThe signed change. See Data-viz.

Number tones

The number takes a tone in its short form (_78-success, _78-danger …) in addition to _78-stat-card__num. These are the semantic colors, so they mean what they always mean — they do not follow your accent.

Accent
$48,290
Success
99.98%
Warn
14 days
Danger
3 failing
stat-card number tones
<div class="_78-kpi-grid">
  <div class="_78-stat-card">
    <div class="_78-stat-card__head"><span class="_78-stat-card__label">Accent</span></div>
    <div class="_78-stat-card__num _78-accent">$48,290</div>
  </div>
  <div class="_78-stat-card">
    <div class="_78-stat-card__head"><span class="_78-stat-card__label">Success</span></div>
    <div class="_78-stat-card__num _78-success">99.98%</div>
  </div>
  <div class="_78-stat-card">
    <div class="_78-stat-card__head"><span class="_78-stat-card__label">Warn</span></div>
    <div class="_78-stat-card__num _78-warn">14 days</div>
  </div>
  <div class="_78-stat-card">
    <div class="_78-stat-card__head"><span class="_78-stat-card__label">Danger</span></div>
    <div class="_78-stat-card__num _78-danger">3 failing</div>
  </div>
</div>

A complete stat card

The delta's color comes from its sign. Add data-invert="true" where a rise is bad, which flips the color but never the arrow — the arrow always tells the truth about direction.

Revenue
$48,290
vs last month
Support tickets
312
up is bad here
KPI cards
<div class="_78-kpi-grid">
  <div class="_78-stat-card">
    <div class="_78-stat-card__head">
      <span class="_78-stat-card__label">Revenue</span>
      <span class="_78-stat-card__icon">
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M3 17l6-6 4 4 8-8"/></svg>
      </span>
    </div>
    <div class="_78-stat-card__num _78-accent">$48,290</div>
    <div class="_78-stat-card__row">
      <span class="_78-stat-delta" data-delta="12.4"></span>
      <span class="_78-stat-card__sub">vs last month</span>
    </div>
    <div class="_78-stat-card__spark" data-values="18,22,19,27,25,31,29,38,44,41,52" data-tone="auto"></div>
  </div>

  <div class="_78-stat-card">
    <div class="_78-stat-card__head">
      <span class="_78-stat-card__label">Support tickets</span>
    </div>
    <div class="_78-stat-card__num _78-success">312</div>
    <div class="_78-stat-card__row">
      <span class="_78-stat-delta" data-delta="8.2" data-invert="true"></span>
      <span class="_78-stat-card__sub">up is bad here</span>
    </div>
    <div class="_78-stat-card__spark _78-tone-danger" data-values="20,22,24,23,27,29,31"></div>
  </div>
</div>

A stat card can also be a link: combine it with _78-card-link and it takes the accent border on hover.

clickable stat card
<a class="_78-stat-card _78-card-link" href="/revenue">…</a>

A control beside the label

A number often has two readings — today or all-time, this month or this year. Put the switch in _78-stat-card__actions at the end of the head. A _78-seg inside it drops to a compact 24px, and the slot keeps the head at the label's own height, so the card's number still lines up with its neighbors' in a grid. An icon button works the same way.

Revenue
$48,290
this month
Active seats
1,204
of 1,500
_78-stat-card__actions
<div class="_78-kpi-grid">
  <div class="_78-stat-card">
    <div class="_78-stat-card__head">
      <span class="_78-stat-card__label">Revenue</span>
      <div class="_78-stat-card__actions">
        <div class="_78-seg" aria-label="Revenue period">
          <button class="_78-seg-btn" type="button" data-value="month" aria-checked="true">Month</button>
          <button class="_78-seg-btn" type="button" data-value="year">Year</button>
        </div>
      </div>
    </div>
    <div class="_78-stat-card__num">$48,290</div>
    <div class="_78-stat-card__sub">this month</div>
  </div>
  <div class="_78-stat-card">
    <div class="_78-stat-card__head">
      <span class="_78-stat-card__label">Active seats</span>
    </div>
    <div class="_78-stat-card__num">1,204</div>
    <div class="_78-stat-card__sub">of 1,500</div>
  </div>
</div>

Keep the control out of _78-stat-card__label itself: the label is uppercase, letter-spaced text, and that styling would reach the buttons.

Key : value lines

Sometimes the card is a label and several figures rather than one big number — the account's change, how it did against a benchmark, how the trades did. _78-stat-card__lines holds them one per row, each a _78-stat-card__key and a _78-stat-card__value. The keys share one column, so every value starts on the same edge however long the keys are. In a value, <b> is the figure (give it a short tone like _78-success) and <small> is the dim aside after it.

Portfolio
$48,290
Account +$123.45 today
vs benchmark +0.50% you +1.2% · index +0.7%
Trades −0.30% on capital deployed, across 9 days
_78-stat-card__lines
<div class="_78-kpi-grid _78-kpi-grid-lg">
<div class="_78-stat-card">
  <div class="_78-stat-card__label">Portfolio</div>
  <div class="_78-stat-card__num">$48,290</div>
  <div class="_78-stat-card__lines">
    <div class="_78-stat-card__line">
      <span class="_78-stat-card__key">Account</span>
      <span class="_78-stat-card__value"><b class="_78-success">+$123.45</b> <small>today</small></span>
    </div>
    <div class="_78-stat-card__line">
      <span class="_78-stat-card__key">vs benchmark</span>
      <span class="_78-stat-card__value"><b class="_78-success">+0.50%</b> <small>you +1.2% · index +0.7%</small></span>
    </div>
    <div class="_78-stat-card__line">
      <span class="_78-stat-card__key">Trades</span>
      <span class="_78-stat-card__value"><b class="_78-danger">−0.30%</b> <small>on capital deployed, across 9 days</small></span>
    </div>
  </div>
</div>
</div>
ClassWhat it does
_78-stat-card__linesThe wrapper: a two-column grid, keys then values, --space-2 under the number.
_78-stat-card__lineOne line. Inside the wrapper it joins the grid; on its own it's a flex row.
_78-stat-card__key--fs-xs, --dim, never wraps.
_78-stat-card__value--fs-sm, --text, tabular figures; <small> inside is the dim aside.

Meta line

_78-meta is the small dim line of facts under a headline — a scan summary, a thesis, the factors behind a score. Write each fact as its own element and the kit draws the · between them, so the markup stays a list of facts instead of a string with the punctuation typed in. The separator has empty alt text, so a screen reader doesn't read it out.

Watch list

Tonight's scan: Scanned 1,234 names read the top 20 added 3 passed 17

Three new names are waiting for a first look.

_78-meta
<div class="_78-card">
  <div class="_78-card-title">Watch list</div>
  <p class="_78-meta">
    <span class="_78-meta-label">Tonight's scan:</span>
    <span>Scanned <b>1,234</b> names</span>
    <span>read the top <b>20</b></span>
    <span>added <b>3</b></span>
    <span>passed <b>17</b></span>
  </p>
  <p>Three new names are waiting for a first look.</p>
</div>

Straight after a _78-card-title or _78-card-head it tucks up under the title and keeps the title's spacing below itself, so it reads as part of the heading. A _78-meta-label lead-in takes no separator after it, and a <b> lifts a figure out of the dim text.

Clamped

A thesis or a list of factors can run long. _78-meta-clamp cuts the line at one with an ellipsis, so the card never grows taller than its neighbors; _78-meta-clamp-2 allows two, and --_78-meta-lines any number.

Watched for: a pullback to the 50-day average on falling volume, then a reclaim of the prior week's high before the earnings date, with the sector holding above its own 200-day line Open the watch list →

Two lines with the custom property: renewal is due in twelve days, the champion moved teams last month, usage is down a fifth since the summer, two open tickets mention the export limit, the last invoice was paid eleven days late, the new procurement contact has not replied to either email, and the account has asked twice about the price of the annual plan against the monthly one

Updated 4 min ago 3 sources _78-meta-xs

clamped and xs
<div class="site-demo-block">
  <p class="_78-meta _78-meta-clamp">
    <span class="_78-meta-label">Watched for:</span>
    <span>a pullback to the 50-day average on falling volume, then a reclaim of the prior week's high
      before the earnings date, with the sector holding above its own 200-day line</span>
    <a href="#meta">Open the watch list &rarr;</a>
  </p>
  <p class="_78-meta _78-meta-clamp" style="--_78-meta-lines: 2">
    <span>Two lines with the custom property: renewal is due in twelve days, the champion moved teams last
      month, usage is down a fifth since the summer, two open tickets mention the export limit, the last
      invoice was paid eleven days late, the new procurement contact has not replied to either email, and the
      account has asked twice about the price of the annual plan against the monthly one</span>
  </p>
  <p class="_78-meta _78-meta-xs">
    <span>Updated <b>4 min</b> ago</span>
    <span>3 sources</span>
    <span>_78-meta-xs</span>
  </p>
</div>
Class / propertyWhat it does
_78-meta--fs-sm, --dim, tabular figures. A · before every child after the first.
_78-meta-labelA lead-in: --text, semibold, no separator after it.
b / strongA figure lifted out of the dim text.
_78-meta-xs--fs-xs — a tally line or a timestamp.
_78-meta-clampOne line, then an ellipsis.
_78-meta-clamp-2Two lines, then an ellipsis.
--_78-meta-linesWith a clamp class: any number of lines.

Card stack

Sibling cards space themselves — but only siblings. When each card in a list is wrapped (in a <form>, or by a template partial that brings its own element), or something hidden sits between them, the + rule can't reach them and the list collapses into one slab. _78-card-stack spaces its children with a gap instead, so it doesn't care what they are.

Northwind

Renewal due in 12 days.

Contoso

Waiting on a signed order form.

_78-card-stack
<div class="_78-card-stack">
  <form>
    <article class="_78-card">
      <div class="_78-card-title">Northwind</div>
      <p class="_78-text-dim">Renewal due in 12 days.</p>
    </article>
  </form>
  <form>
    <article class="_78-card">
      <div class="_78-card-title">Contoso</div>
      <p class="_78-text-dim">Waiting on a signed order form.</p>
    </article>
  </form>
</div>
ClassWhat it does
_78-card-stackA one-column grid with a --space-4 gap. Any child is spaced, card or not; a hidden child takes no gap.
_78-card-stack-smWith it: a tighter --space-3 gap.

Grids

Two auto-fit grids, so you rarely write grid CSS for a dashboard. Both wrap on their own and need no media queries.

ClassMinimum columnUse for
_78-kpi-grid190pxThe default KPI row — two to six across.
_78-kpi-grid-sm150pxMore, narrower stats on one row.
_78-kpi-grid-lg240pxFewer, roomier stats — long numbers.
_78-card-grid200pxGeneral cards, not just stats.

Both are presets of the general _78-grid, so a card in either can take _78-col-span-2 or _78-col-full.

The size modifiers go with _78-kpi-grid, not instead of it:

grids
<div class="_78-kpi-grid _78-kpi-grid-sm"> … </div>
<div class="_78-kpi-grid _78-kpi-grid-lg"> … </div>
<div class="_78-card-grid"> … </div>

Cards in a grid line up

Stacked cards space themselves with a top margin. Inside _78-card-grid, _78-kpi-grid, _78-flex or _78-flex-between that margin is dropped, because the container's gap already does the spacing — so every card in a row sits level with the first.

North

Level with its neighbors.

South

No stacking margin in a grid.

West

The gap spaces the row.

cards in a grid
<div class="_78-card-grid">
  <div class="_78-card">
    <div class="_78-card-title">North</div>
    <p class="_78-text-dim">Level with its neighbors.</p>
  </div>
  <div class="_78-card">
    <div class="_78-card-title">South</div>
    <p class="_78-text-dim">No stacking margin in a grid.</p>
  </div>
  <div class="_78-card">
    <div class="_78-card-title">West</div>
    <p class="_78-text-dim">The gap spaces the row.</p>
  </div>
</div>
Changed in 0.4.0
Before 0.4.0, every card after the first in a _78-card-grid sat 16px low. If you compensated with your own margin-top: 0, it can go. In a grid of your own the kit can't tell, so zero the margin there yourself.