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.
Design Language · v1
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.
01
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.
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.
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.
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.
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.
Tabular numerals, monospace, right-aligned for comparison. Currency, hours, and tenure each have one canonical format — never two on the same page.
If a glyph or numeric badge needs a paragraph to understand, it is the wrong glyph. Tooltips are recovery, not design.
02
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.
03
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.
| Token | Role | Value |
|---|---|---|
| --bg | Page surface | oklch(0.985 0.004 80) |
| --surface | Cards, tables, popovers | #ffffff |
| --surface-2 | Hover, table head, code | oklch(0.975 0.004 80) |
| --border | Hairlines | oklch(0.92 0.005 80) |
| --fg | Primary text, primary button | oklch(0.20 0 0) |
| --fg-2 | Secondary text | oklch(0.42 0.005 80) |
| --fg-3 | Tertiary, captions | oklch(0.58 0.005 80) |
| --fg-4 | Placeholder, disabled, zero values | oklch(0.72 0.005 80) |
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.
Three states only: ok, warn, crit. Each pairs a foreground hue with a soft tinted background for pills, never used on chrome.
| Token | Use | Example |
|---|---|---|
| --ok | Active, healthy, on-shift | Active |
| --warn | Alert filter, expiring, awaiting action | 3 alerts |
| --crit | Destructive, blocked, deactivated | Deactivated |
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
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.
| Step | px | Used for |
|---|---|---|
| 4 | 4 | Inline gaps inside a single component |
| 8 | 8 | Icon ↔ label, chip padding |
| 12 | 12 | Between siblings in a card |
| 16–20 | 16, 20 | Row padding, card padding |
| 28–32 | 28, 32 | Page sections, hero margins |
| 48+ | 48, 64 | Document section dividers |
Sharp at small sizes, soft at large ones. Pills (999px) are reserved for chips, status badges, and the floating bulk-action bar.
| Token | Value | Use |
|---|---|---|
| --r-xs | 4px | Checkboxes, inline pips |
| --r-sm | 6px | Icon buttons |
| --r-md | 8px | Buttons, inputs, popovers |
| --r-lg | 12px | Cards, table container |
| 999px | ∞ | Pills, dots, bulk bar |
05
The product's primary surface. These rules apply to every list of records longer than five rows.
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.
| 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 |
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.
06
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.
07
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.
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.
08
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.
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
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.
10
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.
11
What the next iteration of the page should resolve, ordered by impact.