Consistent the
first time.
A dark, sharp, OKLCH token system for data-dense dashboards. Link two files, drive the theme from three variables, and every screen ships consistent. No hardcoded values, no drift.
- v32026-07-21Left nav rail with scroll-spy, agents.md build contract, starter.html skeleton, self-hosted fonts, palette indexes every token.
- v2.12026-06-16Type and spacing scales bumped. --text-xs became the 13px floor, --text-label the floor for uppercase mono labels.
- v22026-06-05Generator tier added (accent, secondary, and base hue plus contrast). Cyan 195 and neon pink 320 became the palette.
- v12026-03First token set: OKLCH palette, five surface steps, sharp corners, the three-face type system.
Start here
Two stylesheets carry the whole system: fonts, palette, type scale, spacing, and every component on this page. Link them in the head, in this order, then use the tokens.
<link rel="stylesheet" href="https://design.seantippen.com/tokens.css"> <link rel="stylesheet" href="https://design.seantippen.com/components.css">
All three faces load from tokens.css. There is no Google Fonts link to add.
Three rules
var(--token) in CSS and tok('--token') in Chart.js. Never paste a hex, rgba, or oklch value. The one exception is Chart.js dataset colors, which the Canvas API cannot resolve from CSS.0 by default; rounded is reserved for pills, toggles, and circular controls. No gradient page or section backgrounds, no gradient text.Lock dark mode on brand-critical pages
Stops a reader's dark-mode browser from re-mapping the palette and making the design look wrong. Add a theme-color meta matching --color-bg for the address bar.
<meta name="color-scheme" content="dark"> <meta name="darkreader-lock"> <meta name="theme-color" content="#0a0c12">
Color
OKLCH for all custom colors, captioned by the job each one does. Hex only for Chart.js data series (Canvas API). Click anything with a token name to copy its computed value. Drive the hue with the generator above and watch every swatch re-derive.
Brand accents
Surfaces, five-step elevation
Depth comes from lightness, not shadow. App background through floating overlay.
Text hierarchy
Four steps, never pure white. Chrome and labels recede to the lower steps so data leads.
Borders
Neutral white-alpha in dark mode, black-alpha in light. Hue-independent, so they survive any accent swap.
Status
Green success, amber warning, red danger, blue info. These hues are fixed and never accent-swapped, so a status always reads the same regardless of the theme.
Data series (Chart.js palette)
Eight hex colors at the same perceived brightness so no series dominates. Hex because the Canvas API cannot read CSS custom properties at paint time. Click a band to copy its hex.
Contrast, computed live
Recomputed from the active tokens. Drive the generator or toggle the theme and the ratios recalculate.
| Pair | Ratio | Grade |
|---|---|---|
| Body text on canvas | . | |
| Secondary on canvas | . | |
| Muted on canvas | . | |
| Disabled on canvas | . | |
| Accent on canvas | . | |
| Body text on surface | . |
Typography
A committed three-face system: Big Shoulders Display for the one hero header, Inter for everything else, JetBrains Mono for numbers and code. All three are self-hosted from this domain, latin subset, variable weight. Fluid sizing via clamp().
The three faces
Hierarchy specimen
Every step is a token. --text-xs is the absolute floor at 13px; nothing on any page renders smaller. Click a row to copy the token.
Weight scale
Leading and tracking
| Token | Value | Use |
|---|---|---|
| --leading-tight | 1.2 | KPI values, display headings |
| --leading-snug | 1.35 | Card copy, dense descriptions |
| --leading-normal | 1.5 | UI default |
| --leading-relaxed | 1.65 | Long-form body |
| --measure | 65ch | Max reading width |
| --tracking-display | -0.03em | Big Shoulders hero |
| --tracking-tight | -0.01em | Headings |
| --tracking-label | 0.12em | Every uppercase mono label |
Spacing & motion
Fluid spacing scales between viewport poles via clamp(). Motion is token-timed, ease-out by default, spring for delight, always behind prefers-reduced-motion. Click any row to copy its token.
Fluid spacing scale
Fixed spacing, 4px base
Use these when a value must not move with the viewport: icon gaps, control padding, grid gutters inside a component.
Motion tokens
| Token | Value | Use |
|---|---|---|
| --duration-instant | 80ms | Press feedback, tooltip show |
| --duration-fast | 150ms | Hover, focus, small state changes |
| --duration-normal | 250ms | Drawers, view transitions |
| --duration-slow | 400ms | Toasts, page-load reveal |
| --ease-out | decelerate | Default for functional motion |
| --ease-in-out | symmetric | Looping or reversible motion |
| --ease-spring | overshoot | Delight only (toast, palette) |
Shadow and focus
| Token | Use |
|---|---|
| --shadow-sm | Resting control |
| --shadow-md | Raised card, hover lift |
| --shadow-lg | Drawer, modal |
| --shadow-float | Floating panel with a hairline ring |
| --shadow-float-lg | Command palette, large overlay |
| --focus-ring | Always on :focus-visible, never :focus |
| --radius | 0. Sharp is the default |
| --radius-full | Pills, badges, toggle thumbs, dots |
Components
Everything below is rendered from the real components.css classes, the same sheet your dashboard links. Flip any demo to Code and copy the exact markup.
Buttons
Input & chips
Tabs & segmented
Badges & toasts
Empty & error states
Skeleton loading
Drawer
A right-side slide-in for drill-down detail. Scrim closes it, so does Escape.
Data display
KPIs, charts, and tables. No card wrappers; data lives directly on the surface. Numbers are monospace with tabular figures so columns lock. Every table sits in a .table-scroll wrapper and every chart in a .scroll-x wrapper.
KPI markers
Bar chart
Chart.js 4.4.7, series colors via tok() on the hex tokens, global defaults wired to tokens (square corners, mono ticks, 12px floor).
Data table
| Region | Revenue | Growth | Status |
|---|---|---|---|
| Northeast | $842,500 | +12.3% | Growing |
| Southeast | $621,300 | +8.1% | Growing |
| Midwest | $534,700 | +2.4% | Stable |
| West Coast | $412,800 | -3.7% | Declining |
| Total | $2,411,300 | +5.1% |
Info rows
Dashboards
Composite patterns for data-dense surfaces. Every block is a stock component, composed the way a real dashboard composes them. No bespoke CSS.
Composed dashboard
| Rep | Sold | Revenue |
|---|---|---|
| Avery | 1,284 | $58,210 |
| Mateo | 1,102 | $49,870 |
| Priya | 986 | $44,120 |
| Total | 3,372 | $152,200 |
Primitive set
Delta chips, progress, pills, toolbar, stale timestamps. All ship in components.css.
Motion & depth
Felt, not seen. One orchestrated page-load reveal, native scroll-driven section reveals, and View Transitions on theme and accent swaps. Everything respects prefers-reduced-motion.
Hover lift
Depth comes from shadow and a small vertical move. Never scale, never a glow that tracks the cursor.
Frosted glass & shadow elevation
Glass is for overlays only (modals, dropdowns, command palette, the nav rail), never flat cards. Shadows convey hierarchy, not decoration.
Build contract
Four files are the whole consumable surface. Two are stylesheets you link, one is a machine-readable contract an agent can read instead of the CSS, and one is a skeleton you clone.
Compatibility guarantee
Roughly forty projects link these two stylesheets live. Every public custom property name and every component class keeps working; changes are additive. The compatibility aliases (--bg, --surface, --text, --accent, and the rest) are permanent.
Greenville Triumph work runs on a separate system. That one is canonical for anything Triumph-facing: design.greenvilletriumph.club
Anti-patterns
What not to do, and what to do instead. These are the lines between an intentional interface and generic output.
var(--color-bg), or one position:fixed palette-cohesive field that never moves.oklch(0.70 0.25 320) as a state color.transform: scale() on hover; it is janky and displaces layout.translateY(-2px) lift plus a deeper shadow.var(--color-text) or var(--color-accent) with a weight bump.0.65rem to make a dense block fit.--text-label and cut content or widen the column instead..table-scroll so it scrolls inside its own box.Ship checklist
Run this before any dashboard goes live. If every line is true, it ships consistent.
- Both sheets linked,
tokens.cssbeforecomponents.css - No Google Fonts link; the three faces load from
tokens.css - No hex, rgba, or oklch literals in CSS (Chart.js data series excepted)
- Colors via
var(--token), charts viatok('--token') - No raw rem or px
font-size; nothing below--text-xs - Every uppercase mono label at
--text-labelwith--tracking-label - Sharp corners; rounded only for pills, toggles, circles
- One accent per layout; pink as state only, never blurple
- Text never pure white; body and accent clear AAA on the canvas
- Amber reserved for warning states, never decoration
- No gradient text; no gradient page or section backgrounds
- Glass only on nav, rail, drawers, and overlays, never flat cards
- Every table in
.table-scroll, every chart in.scroll-x - No horizontal page scroll at 390px; every section reachable
tabular-numson every numeric column and KPI- Hover lifts with
translateY, neverscale() - Focus rings on
:focus-visible; tap targets 44px or larger prefers-reduced-motionrespected- Dark mode locked on brand-critical pages
- No em-dashes; plain, literal copy
Token reference
The generator tier sits on top: change four variables, re-skin everything. The full set lives in tokens.css, and every one of them is searchable in the command palette (Ctrl K).
/* Generator tier, drives the whole system */ --accent-h: 195 --secondary-h: 320 --base-h: 252 --contrast: 1 /* Accent and surface (derived) */ --color-accent --color-accent-hover --color-accent-border --color-accent-solid-hover --color-secondary --accent-muted --accent-subtle --color-bg --color-surface --color-surface-hover --color-surface-raised --color-surface-overlay /* Text hierarchy */ --color-text --color-text-secondary --color-text-muted --color-text-disabled --text-alpha-90 to --text-alpha-15 /* Borders */ --color-border --color-border-emphasis --color-border-strong /* Status */ --color-success --color-warning --color-danger --color-info (and each -subtle) /* Chart data series (hex, for Chart.js) */ --green --blue --red --amber --purple --cyan --yellow --orange /* Type */ --font-sans --font-mono --font-display --text-xs to --text-4xl /* fluid clamp scale, --text-xs is the floor */ --text-label /* the one uppercase mono label size */ --leading-* --tracking-* --measure /* Spacing / motion / radius / layout */ --space-xs to --space-xl --space-section --space-1 to --space-16 --duration-* --ease-out --ease-spring --radius: 0 --radius-full: 9999px --max-width: 1100px --sidebar-w: 260px /* Compatibility aliases, permanent */ --bg --surface --surface2 --surface3 --surface-hover --text --text-dim --text-muted --text-secondary --text-disabled --accent --accent2 --secondary --border --border-emphasis
Figures above are computed at runtime from the live sheets, not hardcoded. Both files are fetched once and reused for the palette index.