Skip to content

About

Open UI & design reference for Capitol Trace — the React components and design language behind the civic-intelligence platform. Part of the Capitol Trace civic data network.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

🏛️ Capitol Trace UI

The open UI & design reference behind Capitol Trace.
How we make public records legible — components, patterns, and design language.

Live React Vite Tailwind


capitoltrace.com turns 27+ government data sources into a fast, readable civic-intelligence platform — 95+ pages of member dossiers, money maps, and national-security feeds. This repository is the public design reference for that interface: the design language and component patterns we use to turn raw public records into something a citizen can actually read.

Curated, not a full export. The application code stays in the private Capitol Trace monorepo. This repo documents the design system and surfaces self-contained, reusable UI pieces over time — no proprietary data logic, scoring, or prompts.

Design language

  • Stack: React 18 + Vite + Tailwind CSS
  • Theming: CSS-variable overrides with dark / light / system modes
  • Type & color: a restrained, high-contrast system tuned for dense data tables and long reading
  • Principle: receipts, not vibes — every view traces back to a public record

What's here

  • Design language — the color, type, spacing, and theming rules behind the platform (documented here)
  • Component patterns — how we structure dossiers, data tables, and dashboards
  • Curated components (rolling out) — self-contained React + Tailwind pieces, added over time

If you're looking for the full product, it's live at capitoltrace.com — free, no account required.


@capitoltrace/ui

The shared design layer, consumed by both capitoltrace and voterready. Plain JavaScript + JSX, ESM, React 18 peer dependency. No TypeScript.

npm install    # once
npm run build       # dist/index.js + dist/tokens.css
npm run storybook   # http://localhost:6006
npm test            # axe, contrast, and focus-ring assertions

Installing it into an app: see MIGRATION.md.

Entry points

Import What it is
@capitoltrace/ui The twelve primitives — Card, Chip, SourceLine, StatBoard, Button, FilterBar/FilterGroup, Field/Input/Select, DataTable, Tabs, Panel, EmptyState, Skeleton — named exports, no defaults
@capitoltrace/ui/tokens.css Every token as a CSS custom property. Import once at the app root
@capitoltrace/ui/tailwind-preset The same tokens as Tailwind utilities (bg-ct-surface, rounded-ct-card, …)

src/tokens.css is the single source of truth. The Tailwind preset holds no literal colours — every entry is a var(--ct-*) reference.

Theming

Light tokens live on :root. Dark overrides live on html.dark, matching the app's Tailwind darkMode: 'class' setup — dark mode is a dark class on <html>. There is no data-theme attribute anywhere in this package.

Because the selector is html.dark, dark mode cannot be scoped to a subtree. Storybook drives the document element rather than a wrapper, which is why light and dark are separate stories instead of side-by-side panels.

Note: the dark palette here is a new fourth theme introduced by this design system. It intentionally matches none of the app's existing three (newsroom light/default, bloomberg dark, light).

Light → dark mapping

Six core tokens are remapped outright:

Token Light Dark
--ct-bg #f7f7f5 #0d1b2a
--ct-surface #ffffff #11243a
--ct-surface-alt #faf9f7 #16293f
--ct-border #e7e5e1 #22334a
--ct-text #0d1b2a #f1f5f9
--ct-text-secondary #475569 #94a3b8
--ct-accent #b97309 #f59e0b

The desk palette and the semantic pair shift to their 400-weights in dark for legibility. The token name does not change — --ct-desk-money resolves to whichever weight the theme calls for, so components never branch on theme:

Token Light Dark (→ 400) Contrast on dark --ct-surface
--ct-desk-money #0f6e56 #3fb894 6.35:1
--ct-desk-news #a32d2d #e06a62 4.79:1
--ct-desk-civic #185fa5 #4a97e0 5.08:1
--ct-desk-education #534ab7 #9a92e8 5.72:1
--ct-desk-convergence #f59e0b #fbbf24 9.41:1
--ct-positive #0f6e56 #3fb894 6.35:1
--ct-negative #a32d2d #e06a62 4.79:1

Each 400-weight clears 4.5:1 against --ct-surface #11243a. The 400 values are also addressable directly (--ct-desk-money-400, --ct-positive-400, …), as are --ct-dem-400 and --ct-gop-400, which do not remap.

Everything else — --ct-ink-2, --ct-ink-3, --ct-rule-on-ink, --ct-amber, the amber washes, --ct-dem/--ct-gop/--ct-tossup — is theme-invariant and carries its :root value into dark.

Colour rules

Three rules the components enforce and the test suite guards mechanically:

  1. #f59e0b fails AA as text on light surfaces (2.15:1). It is for fills, spines, dots, and text on dark backgrounds only.
  2. --ct-accent #b97309 is 3.80:1 on white and 3.55:1 on --ct-bg. It clears the 3:1 bar for large text (≥24px, or ≥18.66px bold), UI boundaries, and focus rings — and does not clear 4.5:1 for body-size text. The only sanctioned accent-on-light in the primitives: StatBoard's 24px figure (large text) and Tabs' 2px active underline (non-text boundary).
  3. The focus ring uses --ct-accent, never --ct-amber. --ct-accent remaps per theme (3.55:1 light, 8.10:1 dark, both against --ct-bg), so it passes the 3:1 non-text bar in both. Hardcoded --ct-amber would be 2.15:1 in light.

Desk colours are for kickers, 3px card spines, and data marks only. Never a button fill, never a page tint.

The focus ring — 2px --ct-accent at 2px offset — is never removable. No component sets outline: none, and the test suite fails if one starts to.

Known deviations from the supplied token set

One, documented inline in src/tokens.css:

  • --ct-text-muted is aliased to --ct-text-secondary under html.dark. The supplied dark palette does not remap --ct-text-muted, which leaves it at its light #64748b in dark: 3.10:1 on --ct-surface-alt and 3.30:1 on --ct-surface. That is the colour SourceLine (10.5px) and StatBoard labels (10px) are specified to use, so dark mode would fail AA on two of the first four primitives. Aliasing it to the canonical dark --ct-text-secondary #94a3b8 gives 5.76:1 and 6.12:1. No new colour enters the system. Delete the alias and npm test fails with those exact ratios and points back at the line.

Two pairs in the supplied palette pass, but only just — they have no room for a future nudge, and the test suite will catch it the moment one moves:

  • --ct-negative #e06a62 on dark --ct-surface-alt (the ERROR label) — 4.50:1.
  • --ct-text-muted #64748b on light --ct-surface-alt (the SourceLine strip) — 4.52:1.

Also worth knowing: Chip uses --ct-text as its foreground for every tinted tone, not the tone colour. Tinting the label instead would put dem at 3.63:1, gop at 4.00:1, and tossup at 1.64:1 against their own 10% fills at 11px. The tint and the 30% edge carry the meaning; the ink stays readable, and the geometry is identical across tones either way.

Chip tone="accent" carries the package's only hardcoded hex — the foreground is pinned to the literal #0d1b2a rather than --ct-text, because --ct-text remaps to #f1f5f9 in dark and lands at 1.96:1 on amber. It is commented in src/components/Chip.jsx, and a test asserts no other hex appears outside tokens.css.

Type

Inter for language, JetBrains Mono for anything machine-generated — figures, labels, routes, timestamps, kickers. Exposed as --ct-font-sans and --ct-font-mono; the package does not load the fonts, the apps do.

Step Size / line-height / tracking / weight Utility
display 52 / 1.04 / -.025em / 800 text-ct-display
h1 32 / 1.15 / -.02em / 800 text-ct-h1
h2 22 / 1.25 / -.02em / 800 text-ct-h2
h3 17 / 1.35 / 700 text-ct-h3
body 15 / 1.6 / 400 text-ct-body
secondary 13.5 / 1.55 / 400 text-ct-secondary
kicker mono 11 / 700 / .08em uppercase text-ct-kicker
figure mono 26 / 700 tabular text-ct-figure

Every figure gets font-variant-numeric: tabular-nums. The .ct-figure class (in tokens.css) bundles mono + 700 + tabular so a caller can't render a figure and forget it; size stays separate.

Space, radius, elevation

  • Space — 4px base: 4, 8, 12, 16, 20, 32, 56 (p-ct-16, gap-ct-8, …). Container 1240px (max-w-ct-container), gutters 16 / 24.
  • Radius — three values plus pill, role-named so they can't be misapplied: rounded-ct-control (4, buttons/inputs/chips/badges) · rounded-ct-block (8, nested blocks/list rows) · rounded-ct-card (12, cards/panels/tables) · rounded-ct-pill (999, accent bars and status dots only).
  • Elevation — rest is a 1px hairline with no shadow. Hover is translateY(-2px) plus shadow-ct-hover. shadow-ct-overlay is for modals and dropdowns only.

Primitives

Component What it is
Card Surface, 1px border, 12px radius, 20px padding. desk renders the 3px spine; interactive adds the lift and renders a real focusable element; as overrides the element
Chip 4px radius, 11px mono uppercase. One geometry — tone sets a 10% tint and 30% border. accent is the exception: solid amber
SourceLine The receipts strip. Status dot, agency names, filing links, Synced 28 Jul 09:15 UTC — always UTC, always that format
StatBoard A bordered grid of figures with 1px internal dividers. First stat (or any tone="accent") leads in accent; the rest stay --ct-text
Button Four fills at three heights (30/38/44). primary is ink; accent is amber — the one invitation per screen; secondary bordered; ghost bare. Disabled is a state, not a variant; as="a" for link-buttons
FilterBar / FilterGroup Segmented filter groups on a --ct-surface-alt strip with a right-aligned sort slot. Selected segment is an ink fill — never amber
Field / Input / Select Mono uppercase label above a 38px control; errors are mono text beneath, never a tooltip. Field wires id, aria-invalid, and aria-describedby onto its one control child
DataTable The workhorse: mono 10.5px uppercase headers (sorted column goes ink), right-aligned tabular mono figures, zebra on even rows, 44/34px rows via density, controlled sort/onSort, and pagination + sourceLine footer slots. Presentation only
Tabs The detail-archetype strip with optional mono counts. Roving tabindex, automatic activation, arrow/Home/End keys; the active underline is --ct-accent
Panel Dashboard building block: header row (title + actions), free-form body, sourceLine footer slot. Grid spans (4/6/12) are the consumer's grid classes
EmptyState Icon-less: title, one-line body, at most one action. Transparent — inherits the surface it sits on
Skeleton text/figure/block bones sized by the type scale. Opacity-only pulse, off under prefers-reduced-motion. Skeletons, never spinners — the layout must not reflow when data lands

Each one: named export, React.forwardRef, className and ...rest forwarded to the root, no default export, no data fetching, and no margin on the outermost element — spacing is the caller's job.

DataTable, FilterBar, Field, Tabs, Panel, EmptyState, and Skeleton landed in 0.2.0 (phase 3). The spec's "Desk card" and error/cached-data states are compositions, not components — see the DeskCard story (Card + SourceLine) and the DataTable CachedData/ErrorState stories (SourceLine status + EmptyState).

Tests

npm test runs the suites over every story and every token pair:

  • test/a11y.test.jsx — axe over all 127 stories, no violations.
  • test/contrast.test.js — every foreground/background pair the primitives can produce, in both themes, classified by the size it is actually rendered at: 4.5:1 for body text, 3:1 for large text and non-text boundaries. A flat 4.5:1 bar would wrongly fail StatBoard's 24px accent figures and the focus ring. Purely decorative pairs (container hairlines, redundant spines and dots) are measured and reported with a written exemption rather than asserted.
  • test/focus.test.jsx — the focus ring resolves to --ct-accent in both themes, every focusable element in every story carries it, and no component suppresses the outline.
  • test/dataTable.test.jsx and test/tabs.test.jsx — behavior contracts: sort wiring, density heights, zebra, footer slots; roving tabindex and arrow-key activation.

Contrast is read from the real src/tokens.css, not a copy — change a token and the test inputs change with it.


Part of the Capitol Trace civic data network.

About

Open UI & design reference for Capitol Trace — the React components and design language behind the civic-intelligence platform. Part of the Capitol Trace civic data network.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages