Design System · v3 · Dark-first

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.

Tickets · Season 2026 Live 2m ago
Revenue
$284K8.2%
Tickets
8,14212%
Fill
94%0.0%
Supporters88%
Premium34%
Drive the system. Three variables re-skin every surface, border, and accent on this page. Blurple (hue 270 to 285) is out of range by design.
Accent hue195
Secondary hue320
Contrast1.00
v3 2026-07-21 Current release. Public token names and component classes never break; the compatibility aliases stay forever.
  • 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.
00

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

01
Tokens, never literals
Use 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.
02
OKLCH, sharp by default
Custom colors are OKLCH for perceptual uniformity. Corners are 0 by default; rounded is reserved for pills, toggles, and circular controls. No gradient page or section backgrounds, no gradient text.
03
Cyan leads, pink supports
One accent per layout. Cyan is the signature; neon pink is a state and second-series color, used sparingly. Never blurple. Amber is a warning state, never decoration.

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">
01

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

--color-accent
Solid fill, active state, the one rationed color
--color-accent-hover
Hover on accent surfaces
--color-accent-solid-hover
Hover on a solid accent fill
--color-accent-border
Interactive border, focus ring
--color-secondary
Neon pink. Second series or state only
--accent-muted
Selected background tint
--accent-subtle
Faint accent wash

Surfaces, five-step elevation

Depth comes from lightness, not shadow. App background through floating overlay.

App bg
--color-bg
Surface
--color-surface
Hover
--color-surface-hover
Raised
--color-surface-raised
Overlay
--color-surface-overlay

Text hierarchy

Four steps, never pure white. Chrome and labels recede to the lower steps so data leads.

--color-text
High-contrast body and headings
--color-text-secondary
Supporting copy
--color-text-muted
Labels, captions, chrome
--color-text-disabled
Disabled, ghost

Borders

Neutral white-alpha in dark mode, black-alpha in light. Hue-independent, so they survive any accent swap.

--color-border
Default hairline, table rows
--color-border-emphasis
Controls, table head rule
--color-border-strong
Hover on a bordered control

Status

Completed Pending Overdue In review Synced Delayed Failed Queued
--color-success
Positive delta, healthy state
--color-warning
Warning state only, never decoration
--color-danger
Failure, negative delta
--color-info
Neutral notice
--color-success-subtle
Badge background
--color-warning-subtle
Badge background
--color-danger-subtle
Badge and error-state background
--color-info-subtle
Badge background

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.

green
blue
red
amber
purple
cyan
yellow
orange

Contrast, computed live

Recomputed from the active tokens. Drive the generator or toggle the theme and the ratios recalculate.

PairRatioGrade
Body text on canvas.
Secondary on canvas.
Muted on canvas.
Disabled on canvas.
Accent on canvas.
Body text on surface.
02

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

Big Shoulders Display · --font-display · hero only
Consistent the first time
Inter · --font-sans · body and UI
The quick brown fox jumps over the lazy dog
JetBrains Mono · --font-mono · numbers and code
$2,847,392.00

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.

Display 4XL
Page title 3XL
Section title 2XL
Subsection XL
Lead paragraph LG
Body base at the measure of 65ch. Generous line-height keeps long reading calm; the measure token stops lines from running too wide to scan.
Small SM, supporting copy and dense tables.
Label, the floor for uppercase mono
XS, captions and metadata. The absolute floor.

Weight scale

Light, large lede only300
Regular body400
Medium, active state500
Semibold, card titles600
Bold, subheads700
Extra bold, display800

Leading and tracking

TokenValueUse
--leading-tight1.2KPI values, display headings
--leading-snug1.35Card copy, dense descriptions
--leading-normal1.5UI default
--leading-relaxed1.65Long-form body
--measure65chMax reading width
--tracking-display-0.03emBig Shoulders hero
--tracking-tight-0.01emHeadings
--tracking-label0.12emEvery uppercase mono label
03

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

--space-xs
clamp(0.375rem, 0.3rem + 0.2vw, 0.625rem)
--space-sm
clamp(0.625rem, 0.5rem + 0.45vw, 1rem)
--space-md
clamp(1.125rem, 0.95rem + 0.6vw, 1.75rem)
--space-lg
clamp(1.75rem, 1.4rem + 1.1vw, 3rem)
--space-xl
clamp(2.5rem, 1.9rem + 2.2vw, 5rem)
--space-section
clamp(4rem, 2.75rem + 5vw, 7rem)

Fixed spacing, 4px base

Use these when a value must not move with the viewport: icon gaps, control padding, grid gutters inside a component.

--space-1
0.25rem
--space-2
0.5rem
--space-4
1rem
--space-8
2rem
--space-16
4rem

Motion tokens

TokenValueUse
--duration-instant80msPress feedback, tooltip show
--duration-fast150msHover, focus, small state changes
--duration-normal250msDrawers, view transitions
--duration-slow400msToasts, page-load reveal
--ease-outdecelerateDefault for functional motion
--ease-in-outsymmetricLooping or reversible motion
--ease-springovershootDelight only (toast, palette)

Shadow and focus

TokenUse
--shadow-smResting control
--shadow-mdRaised card, hover lift
--shadow-lgDrawer, modal
--shadow-floatFloating panel with a hairline ring
--shadow-float-lgCommand palette, large overlay
--focus-ringAlways on :focus-visible, never :focus
--radius0. Sharp is the default
--radius-fullPills, badges, toggle thumbs, dots
04

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

Active ×All regionsLast 30 days

Tabs & segmented

Badges & toasts

CompletedPendingOverdueIn review

Empty & error states

No matches yet
Once tickets sell for this event, activity appears here.
Could not load data
The upstream API returned a 502. This usually clears within a minute.

Skeleton loading

Drawer

A right-side slide-in for drill-down detail. Scrim closes it, so does Escape.

05

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

Total revenue
$2.4M
Tickets sold
18,472
Avg order
$128
Sell-through
94.2%

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

RegionRevenueGrowthStatus
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

Sold3,142
Revenue$42,580
Fill88%
06

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

Tickets · Season 2026
Revenue
$284,6108.2%
vs last week
Tickets
8,14212%
sold this month
STH base
2,3574%
season holders
Fill
94.2%0.0%
last match
RepSoldRevenue
Avery1,284$58,210
Mateo1,102$49,870
Priya986$44,120
Total3,372$152,200

Primitive set

Delta chips, progress, pills, toolbar, stale timestamps. All ship in components.css.

12.3%3.7%0.0% LiveSyncedDelayedFailed 2m ago14m ago2h ago
Supporters88%
Premium34%
Active ×All regions
Updated 2m ago
07

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.

Hover me
translateY plus a deeper shadow.
And me
Border picks up the accent.
Lift, never scale
Scaling text resamples it and reads cheap.

Frosted glass & shadow elevation

Glass is for overlays only (modals, dropdowns, command palette, the nav rail), never flat cards. Shadows convey hierarchy, not decoration.

Frosted overlay
backdrop-filter blur over a busy backdrop. Reserved for floating surfaces.
Shadow SM
Shadow MD
Shadow LG
08

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.

tokens.css
Palette, generator tier, type scale, spacing, motion, radius, and the three self-hosted faces. Link it first.
View file
components.css
Every component class on this page: nav rail, sections, cards, tables, KPIs, states. Link it after tokens.css.
View file
agents.md
The build contract. Head boilerplate, hard rules, token digest, page shell, the full class registry, and Chart.js wiring. Enough to build a correct page without loading the CSS.
Read contract
starter.html
A clonable dashboard skeleton: rail nav, stat band, card grid, scroll-wrapped table, one wired chart. Copy it and edit content only.
Open starter

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

09

Anti-patterns

What not to do, and what to do instead. These are the lines between an intentional interface and generic output.

Don't
Scroll-with-content gradient backgrounds that shift how content reads.
Do
Flat var(--color-bg), or one position:fixed palette-cohesive field that never moves.
Don't
Blue-purple gradient hero (blurple, hue 270 to 285). The single most recognizable AI tell.
Do
One rationed cyan accent; neon pink oklch(0.70 0.25 320) as a state color.
Don't
Puffy uniform rounded cards as the whole layout.
Do
Sharp corners by default; rounded only for pills, toggles, and circular controls.
Don't
transform: scale() on hover; it is janky and displaces layout.
Do
translateY(-2px) lift plus a deeper shadow.
Don't
Gradient text on headers; it looks cheap and breaks on some backgrounds.
Do
Plain var(--color-text) or var(--color-accent) with a weight bump.
Don't
Shrink a label to 0.65rem to make a dense block fit.
Do
Hold --text-label and cut content or widen the column instead.
Don't
Let a wide table push the whole page into horizontal scroll.
Do
Wrap it in .table-scroll so it scrolls inside its own box.
Don't
Catalog every effect at once; it signals "I can do effects," not taste.
Do
One orchestrated load reveal and one canonical hover. Restraint reads as considered.
10

Ship checklist

Run this before any dashboard goes live. If every line is true, it ships consistent.

  • Both sheets linked, tokens.css before components.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 via tok('--token')
  • No raw rem or px font-size; nothing below --text-xs
  • Every uppercase mono label at --text-label with --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-nums on every numeric column and KPI
  • Hover lifts with translateY, never scale()
  • Focus rings on :focus-visible; tap targets 44px or larger
  • prefers-reduced-motion respected
  • Dark mode locked on brand-critical pages
  • No em-dashes; plain, literal copy
11

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
Tokens
.
tokens.css
.
components.css
.
Components
.
Surface steps
5

Figures above are computed at runtime from the live sheets, not hardcoded. Both files are fetched once and reused for the palette index.