Design Language · v1

Quiet tables for busy operators.

This document records the principles, tokens, and patterns the Team product runs on. It exists so that future screens look like they grew on the same plant: same air, same rhythm, same answers to the same recurring questions.

ScopeTeam · Roster · Members
StatusWorking draft
OwnersDesign + Eng

01

Principles

The handful of beliefs that everything else descends from. If a decision conflicts with a principle, the principle wins.

Our users open the team page to do something — pay someone, hire someone, fix a permission. The page should disappear underneath the task.

01

Information density without visual density.

Operators scan many rows quickly. Use generous whitespace, hairline rules, and a quiet palette so the eye can move. Loudness is reserved for the few things that actually demand attention.

02

Every column earns its place.

A column that is empty on most rows is not a column — it is a maintenance burden. Promote it to a row-detail view, hide it when context already implies it, or remove it.

03

Don't repeat the header in every cell.

If the column says PAY RATE, the cells don't need /hr on every row. Units belong in the header or a single hint, not as cell noise.

04

Color carries meaning, not decoration.

Hues are spent on role identity and status, never on chrome or accents-for-vibes. A second use of the same hue means the same thing somewhere else.

05

Numbers are typography.

Tabular numerals, monospace, right-aligned for comparison. Currency, hours, and tenure each have one canonical format — never two on the same page.

06

Affordances explain themselves.

If a glyph or numeric badge needs a paragraph to understand, it is the wrong glyph. Tooltips are recovery, not design.

02

Type

Two families. One sans for everything human, one mono for everything counted.

Geist Sans for prose, labels, and identity. Geist Mono for currency, hours, tenure, IDs, and keyboard hints. Mono is also a visual cue — when you see it, you can compare it numerically with the cell above.

Specimen
Display28 / 600
Team
Section20 / 600
Active members
Title15 / 500
Penny Wiseman
Body14 / 400
Manages scheduling and approves time off for the floor.
Caption13 / 400
Joined May 20, 2008 · 17 years
Meta12 / 400
pwise@buyer.example
Eyebrow11 / 500
Projected weekly payroll
NumericMono · tnum
$25.00   45 hrs   17y 0mo

Rules

  • Never truncate names. Names are identity. Columns must flex to fit them, or the layout is wrong.
  • One unit per page, in the header. Currency suffix lives in the column header or a single light hint, not in every cell.
  • Tabular numerals always for currency, hours, percentages, dates rendered as numbers, and counts in legends.
  • No font weight below 400 at any size — operator-facing copy must remain legible on glare and in poor light.

03

Color

A near-neutral surface palette with two semantic axes: role and status.

Backgrounds and text are warm off-whites and near-blacks. Saturation is forbidden in chrome. Hue only appears where it is a key — a role dot, a status pill, an alert filter.

Surface & text

TokenRoleValue
--bgPage surfaceoklch(0.985 0.004 80)
--surfaceCards, tables, popovers#ffffff
--surface-2Hover, table head, codeoklch(0.975 0.004 80)
--borderHairlinesoklch(0.92 0.005 80)
--fgPrimary text, primary buttonoklch(0.20 0 0)
--fg-2Secondary textoklch(0.42 0.005 80)
--fg-3Tertiary, captionsoklch(0.58 0.005 80)
--fg-4Placeholder, disabled, zero valuesoklch(0.72 0.005 80)

Roles — the only saturation we spend

Every role gets one quiet hue. The dot, the legend bar segment, and the role label all use the same token. New roles inherit --role-default until a hue is assigned.

Owner
--role-owner
oklch(0.30 0.04 280)
Manager
--role-manager
oklch(0.55 0.12 280)
Shift Lead
--role-lead
oklch(0.65 0.12 65)
Buyer
--role-buyer
oklch(0.60 0.10 175)
Cashier
--role-cashier
oklch(0.62 0.10 25)

Status

Three states only: ok, warn, crit. Each pairs a foreground hue with a soft tinted background for pills, never used on chrome.

TokenUseExample
--okActive, healthy, on-shiftActive
--warnAlert filter, expiring, awaiting action3 alerts
--critDestructive, blocked, deactivatedDeactivated

Accent

One indigo, reserved for focus rings, the active sort arrow, and the selected-row tint. Not used on buttons or links — buttons use neutral surfaces.

04

Density

Three settings, one knob. The default is comfortable; compact is for power users; spacious is for projection.

Row padding is the only thing that changes. Hairlines, fonts, and column widths stay constant so muscle memory and column scanning don't break between modes.

Compact · 9px
Sandy Beach17.50
Summer Camp17.50
Destiny Chen17.50
Default · 14px
Sandy Beach17.50
Summer Camp17.50
Destiny Chen17.50
Spacious · 20px
Sandy Beach17.50
Summer Camp17.50
Destiny Chen17.50

Spacing scale

SteppxUsed for
44Inline gaps inside a single component
88Icon ↔ label, chip padding
1212Between siblings in a card
16–2016, 20Row padding, card padding
28–3228, 32Page sections, hero margins
48+48, 64Document section dividers

Radii

Sharp at small sizes, soft at large ones. Pills (999px) are reserved for chips, status badges, and the floating bulk-action bar.

TokenValueUse
--r-xs4pxCheckboxes, inline pips
--r-sm6pxIcon buttons
--r-md8pxButtons, inputs, popovers
--r-lg12pxCards, table container
999pxPills, dots, bulk bar

05

Tables

The product's primary surface. These rules apply to every list of records longer than five rows.

Column policy

A column is a commitment. Before adding one, answer: is this populated for the majority of rows, and is it scannable in a single glance? If either answer is no, move it.

  • Most rows populated → keep as a column.
  • Fewer than ~30% populated → move to a row-expand or detail panel; surface only when populated as an inline glyph next to a related field.
  • Determined entirely by current filter → hide. A Status column on the Active tab is noise.
  • Photo is not a column header. The avatar sits in the identity cell with the name; the column has no label.

Cell formatting

Roster row · default density
Role Name Pay rate ($/hr) Hrs req Tenure
Owner
Penny Wisemanpwise@buyer.example
25.00 20
May 20, 200817 years
Buyer
Summer Campscamp@buyer.example
17.50 40
Jun 5, 20214 yr 11 mo
Do
25.00
Unit lives in the header, e.g. Pay rate ($/hr). Cells stay quiet and align by decimal.
× Don't
$25.00/hr
Currency symbol and unit in every cell creates jagged columns and dulls the eye to actual differences.
Do
17 years   ·   4 yr 11 mo
Tenure rounds to whole years past 24 months. Below 24 months, use {Y} yr {M} mo. Never 17y 12mo.
× Don't
17y 12mo   ·   8y 0mo
Twelve months should roll into a year. Zero months should disappear. Time math errors are a trust tax.

Sorting

Multi-column sort is supported. The active sort priority appears as a small labelled ordinal next to the column name — not a bare number. On hover, the column reveals an arrow; on click, the column joins the sort stack at the next free priority.

Do
Role SORT 1 ↑
Explicit ordinal with a label and direction arrow. Reads correctly even without a tooltip.
× Don't
Role 1 ↕
A bare 1 ↕ looks like decoration. Operators don't know it means "first sort key" unless told.

Row interaction

  • Hover tints the row with --surface-2 and reveals row-end actions.
  • Select tints with --accent-soft and surfaces the floating bulk-action bar.
  • Alert draws a 2px inset of --warn on the left edge of the row — never a full-row tint.
  • Inactive rows demote text two tonal steps; the avatar status dot turns gray.

06

Status & badges

How we say someone is active, alerted, or gone, without shouting.

The avatar status dot (bottom-right of the photo) is the primary status indicator. The pill is the secondary signal, used only when status varies on the visible page. When the filter pins status to a single value, the pill column is removed.

PW
ActiveDot only — works at any density
Active Pill — only when mixed states are shown
Alert Warn pill — only with a noun

Badge anatomy

  • Height 20px, padding 0/8, radius 999px.
  • 10px uppercase with 0.06em tracking — letterforms must remain readable.
  • Soft tint background + saturated foreground, both from the same hue token (--ok + --ok-soft).
  • Never bare color — every badge has a noun in it. A green dot alone is the dot pattern, not a badge.

07

Empty & null states

How a missing value looks, and when the answer is to remove the field instead.

A dash means "no value yet". An en-dash is shown only when the field could be populated; otherwise the cell stays blank. A column of mostly dashes is always wrong.

Do
Penny Wiseman no positions assigned
When most rows have no positions, drop the column and surface a quiet inline note only on rows that need attention.
× Don't
Positions — — — — — —
A column that is dashes 17 rows out of 18 has failed its job — the column itself, not the data.

Zero handling

Zero is a value, but not a loud one. In activity counters (Hires, Deactivations, Birthdays), zeros take --fg-4 and weight 500, while non-zero values take --fg at 600. A card with three zeros and a three should make the three read first.

Last 30 days
0Hires
0Deactivations
0Birthdays
3Anniversaries

08

Icons & glyphs

Icons are shortcuts, not puzzles. If we use one, we name it nearby.

A glyph in a cell must either be paired with a label, replace a word the user already knows from a legend, or be a status indicator (dot, dot-with-color). The Access column is the canonical bad case — a purple key and a blue hash with no anchor anywhere on the page.

Do
KEY PIN
Short text chips read instantly. Hue is reserved for whether the access is granted.
× Don't
🔑 #
Two unlabeled glyphs in two unrelated hues require the reader to memorize a private vocabulary.

Tag glyphs on identity

Inline glyphs next to a name — a small key for keyholders, a cake for a birthday this week — are allowed because they sit beside the name they describe, and they replace a longer "(keyholder)" suffix. Keep them tiny (12px), use --fg-4 tint, never more than two per name.

09

Summary cards

The three cards above the roster set the page's emotional tone. They must be answerable in one glance.

Each card has a label, a single dominant number, and at most one secondary element (a breakdown bar, a range, a sparkline). If a card carries four equal-weight numbers, it's not a card — it's a small table, and should be presented as one.

  • One headline number per card. Anything competing for that role belongs below the fold.
  • Ranges must explain themselves. A min–max bar needs a one-line caption stating what bounds the range and where the current value sits within it.
  • Zero-heavy cards demote. See §07 — emphasize the value that did move.
  • No card without a verb. If the user can't take an action from this number — even a mental one — it's vanity.
Carry-over for the next pass: the Projected Weekly Payroll range bar is currently silent. Add a caption like "midpoint of approved hours × hourly rate" so the bar reads as confidence, not as a slider.

10

Voice

How we name things, and how we don't.

Operators say things like "shift lead", "hours requested", and "keyholder". We use their vocabulary, not HR's. Avoid product-speak nouns that exist only inside the app.

  • Sentence case for everything except eyebrows. Eyebrows are 11px uppercase with 0.06em tracking.
  • Numbers come first when the answer is a number. "18 members · 7 keyholders", not "Members: 18 (keyholders: 7)".
  • Time uses long form in body copy ("4 years 11 months"), short form in table cells ("4y 11mo" — but only after the formatting rule in §05 is satisfied).
  • Currency without trailing zeros in display ("$25"), with trailing zeros in tabular numerics ("25.00").
  • No exclamation marks, ever. No celebratory copy on errors. No "Oops!" on failures.

11

Open work

What the next iteration of the page should resolve, ordered by impact.

  1. Let the Name column flex; stop truncating identity.
  2. Drop unit suffixes inside Pay Rate and Hrs Req cells.
  3. Hide the Status column on the Active tab.
  4. Move Positions and Daily Report into a row-expand panel.
  5. Replace the bare sort-priority numerals with labeled ordinals.
  6. Fix tenure formatting — twelve months should roll, zero months should disappear.
  7. Replace Access glyphs with labeled chips, or expose a one-line legend above the table.
  8. Caption the Projected Payroll range bar.
  9. Demote zeros in the Last 30 Days card.
  10. Remove the PHOTO column header.