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.
| Status | Document | Sent | Pages |
|---|---|---|---|
| Signed | Data-access agreementMarch organizing team | Jun 12, 2026 | 4 |
| Ready to sign | Volunteer code of conductSunrise Beagle Brigade | Jun 18, 2026 | 2 |
| Draft | Foster home checklistDowntown chapter | — | 11 |
| Signed | Adoption consent formAdoption events crew | Jun 9, 2026 | 3 |
| Draft | Liability waiverSaturday neighborhood march | — | 1 |
- <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.
- 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.
| Status | Document | Sent | Pages |
|---|---|---|---|
| Signed | Data-access agreementMarch organizing team | Jun 12, 2026 | 4 |
| Ready to sign | Volunteer code of conductSunrise Beagle Brigade | Jun 18, 2026 | 2 |
| Draft | Foster home checklistDowntown chapter | — | 11 |
| Signed | Adoption consent formAdoption events crew | Jun 9, 2026 | 3 |
| Draft | Liability waiverSaturday neighborhood march | — | 1 |
- 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.
- .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.
| Status | Document | Sent | Pages |
|---|---|---|---|
No documents yet Documents you send for signature will show up here with their status. | |||
- <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.