Resolved: light

Data & tables

How tabular data reads in the Save the Dogs skin. There is no Table component — you write a plain, semantic <table>, and globals.css does the header styling for you: the global th rule makes every column header uppercase, letter-spaced and faint, and table is set to border-collapse. So the demos below write raw <table> / <thead> / <th> / <td> and never re-skin the header — they only add the cell box model (padding + alignment) and layer body rows, zebra striping, row hover and the empty state on top with the Save the Dogs surface tokens. Figures use tabular-nums so digits line up column-to-column. Everything is theme-aware (colors resolve from the CSS vars) — flip the theme in the top bar to confirm the header, rules, stripes and hover all re-skin.

no <Table> component
There is no shared Table primitive. You compose a semantic <table>/<thead>/<tbody>/<th>/<td> directly. The skin lives in globals.css, not a wrapper component.
th (global rule)
globals.css styles th GLOBALLY: font-weight 600, font-size 12px, text-transform uppercase, letter-spacing 0.04em, color var(--color-faint). Do NOT re-declare those — just add cell padding + alignment (th sets no text-align).
border-collapse
globals.css sets table { border-collapse: collapse }, so adjacent cell borders merge into single hairlines — use --color-rule divider borders between rows rather than per-cell boxes.
.tabular-nums
A globals.css helper (font-variant-numeric: tabular-nums). Apply to any cell/column of figures (counts, dates, money) so digits are fixed-width and right-aligned columns line up.
status cell
Use StatusPill (or the smaller Badge) in a Status column — never raw colored text. The pill carries a text label so meaning survives without color; the tone (green/amber/gray) is reinforcement, not the only signal.
empty state
When a table has no rows, render EmptyState (calm, neutral — NOT an error look) in place of the body, not an empty grid. Pair it with the table head so the columns still read.

Base table — Status / Document / Sent

The canonical document table: a plain <table> with a thead (uppercase, tracked, faint — straight from the global th rule) and a tbody whose rows are separated by --color-rule hairline borders. The Status cell uses a StatusPill in one of three states — Signed (green), Ready to sign (amber), Draft (gray); the Pages figure uses tabular-nums and is right-aligned so the digits line up. Nothing here re-skins the header — the columns get their look entirely from globals.css.

Documents by status
StatusDocumentSentPages
Signed
Data-access agreementMarch organizing team
Jun 12, 20264
Ready to sign
Volunteer code of conductSunrise Beagle Brigade
Jun 18, 20262
Draft
Foster home checklistDowntown chapter
11
Signed
Adoption consent formAdoption events crew
Jun 9, 20263
Draft
Liability waiverSaturday neighborhood march
1
<table> + global th styling + StatusPill status cell
<thead> / <th>
Each <th scope="col"> gets only px-4 py-2.5 + text-left (or text-right for figures). The uppercase / 12px / 0.04em tracking / faint color all come from the global th rule — don't repeat them.
<tbody> dividers
divide-y divide-[var(--color-rule)] on the tbody draws the hairline between rows (the same rule lines as the dense-row lists). A border-b border-[--color-border] under the header row separates head from body.
Status column
A StatusPill tone="green|amber|gray" size="sm" with a leading dot — Signed / Ready to sign / Draft. The label text means the row is legible in greyscale; the tone is reinforcement.
figure cells
Pages is text-right + tabular-nums, and the Sent dates carry tabular-nums too — fixed-width digits keep the columns aligned row to row even with different values.

Status cell — StatusPill vs Badge

The two status markers you can drop into a Status column, shown across all three document states. StatusPill is the larger, status-forward marker (rounded-full, supports a leading dot and an optional live pulse) — the default for a Status column. Badge is the smaller, quieter rounded-sm tag for tighter tables or inline meta. Both share the exact same semantic tone keys (gray / amber / green — plus red / blue / accent), so they re-skin identically with the theme. Each row maps the same state to the same tone in both components.

SignedReady to signDraft
StatusPill — size="sm" dot (the Status-column default)
SignedReady to signDraft
Badge — size="sm" (the quieter, tighter tag)
Awaiting signatureSyncing
StatusPill pulse — for a live / in-progress row
graygreenamberredblueaccent
full tone set — shared by StatusPill + Badge
StatusPill
The status-forward marker: rounded-full, sizes sm | md | lg (default md), an optional leading dot (dot) and an optional motion-safe pulse (pulse, implies dot) for live/in-progress rows. Reach for it in a Status column.
Badge
The quieter tag: rounded-sm, sizes sm | md (default sm), no dot/pulse. Use for inline meta or very tight tables where the larger pill would crowd the row.
shared tones
Both take tone = gray | green | amber | red | blue | accent, mapped to the same semantic backgrounds (--color-good-bg / -warn-bg / -bad-bg, accent-soft, surface-3). Pick by meaning, not by hue.
never color alone
Every pill/badge here carries a text label, so a row's status survives in greyscale or for color-blind users. tone reinforces the label — it is not the sole signal.

Striped & row-hover variant

The same document table with two scan aids layered on: zebra striping (every other body row gets a bg-surface-2 fill) and a row hover (the whole row lifts to bg-surface-2 under the cursor, transition-colors). Both are pure presentation — they help the eye track across wide rows — and both resolve from the surface tokens, so they re-skin with the theme. Hover any row below to see the fill respond. The header is untouched: still the plain global th styling.

Documents by status — striped and hoverable
StatusDocumentSentPages
Signed
Data-access agreementMarch organizing team
Jun 12, 20264
Ready to sign
Volunteer code of conductSunrise Beagle Brigade
Jun 18, 20262
Draft
Foster home checklistDowntown chapter
11
Signed
Adoption consent formAdoption events crew
Jun 9, 20263
Draft
Liability waiverSaturday neighborhood march
1
odd:bg-surface-2 striping + hover:bg-surface-2 rows
zebra striping
odd:bg-surface-2 on each <tr> tints alternating rows one step up from the bg-surface table. surface-2 is the lightest raised neutral, so the stripe is subtle in both light and dark.
row hover
hover:bg-surface-2 + transition-colors lifts the whole row under the cursor. Pair it with striping (hover wins on the un-striped rows; matches on the striped ones) or use it alone for a hover-only table.
no per-cell borders
Because table is border-collapse, lean on the row fill (stripe/hover) and the tbody divide-y rule for separation rather than boxing each cell — vertical gridlines read as noise in this skin.
presentation only
Striping and hover are scan aids, not state. Don't use them to encode meaning (selected, error, etc.) — that's what the Status pill and row tones are for.

Figures — tabular-nums alignment

Why figures get .tabular-nums. The same three numbers are shown with proportional digits (the default) and with tabular-nums; in the tabular column the ones, tens and hundreds line up into clean vertical rulers, which is what makes a right-aligned numeric column scannable. Apply .tabular-nums to any cell or column of counts, dates or money — it's a globals.css helper, so no import is needed.

71482,394
default (proportional digits) — ragged
71482,394
.tabular-nums — fixed-width digits, aligned
.tabular-nums
A globals.css utility: font-variant-numeric: tabular-nums. Forces every digit to the same advance width so stacked figures align — the foundation of a readable numeric column.
where to apply
Any column of figures: counts (members, pages), dates, currency, percentages. In the demo tables it's on the Pages cell (text-right) and the Sent dates.
pair with text-right
Numeric columns are right-aligned (text-right) so the units, tens and hundreds stack; tabular-nums then makes that stacking pixel-true. Headers for those columns are right-aligned too.

Empty-table state — EmptyState

When a table has no rows, don't render an empty grid — render the EmptyState component (calm and neutral, NOT an error look — that's ErrorState's job). The pattern keeps the table head so the columns still read, then drops a single full-width cell containing EmptyState in place of the body. EmptyState takes an optional icon, a title, an optional description and an optional action node.

Documents — empty state
StatusDocumentSentPages

No documents yet

Documents you send for signature will show up here with their status.

table head + EmptyState in a full-span body cell
<EmptyState>
The canonical no-data state — py-12 text-center, an optional faint icon, a title (font-medium), an optional muted description and an optional action. Calm and neutral by design; use ErrorState (not this) for failures.
colSpan
Put EmptyState in a single <td colSpan={N}> spanning all columns (here 4) so it centers across the whole table width instead of squeezing into the first column.
keep the head
Render the <thead> above the empty body so the column headers still communicate what the table will hold once it has rows — the empty body is the only thing that changes.
action
Optional. A single primary affordance to get out of the empty state (here a “New document” Button). Omit it for purely informational empties where there's nothing to do.