Skip to content

Design system

The site is built from a small system: tokens, a few layout primitives, the components the site actually uses, and three rules. Everything on this page is rendered from the same sources the site uses, so it can’t disagree with them.

The nine marks are the only colour. Everything else is a quiet neutral, so the marks read as what they are: the whole language, one character per state. They appear as a set in one component, MarkArray, and their colours drive the code highlighting, the plan view, the social image and the favicon.

Each mark’s colour is set in OKLCH at one lightness and one chroma per theme, so none is louder than another. To do and dropped are deliberately neutral: the default state and the dead one. Every mark clears 4.5:1 against both surfaces in both themes, and scripts/contrast.mjs fails the build if one stops doing so. The ratios below are measured against the page surface.

TokenLightDark
--mark-todo #191816 17.00:1 #e8e8e5 15.60:1
--mark-doing #915200 5.90:1 #e8a95c 9.36:1
--mark-done #137738 5.40:1 #7ccd8e 10.02:1
--mark-dropped #64635f 5.76:1 #a5a5a1 7.75:1
--mark-later #2863ab 5.81:1 #82baff 9.50:1
--mark-ask #6f4fa1 6.08:1 #c3a5f9 9.19:1
--mark-answer #973f6e 6.19:1 #f197c2 9.07:1
--mark-group #00787a 5.07:1 #3bcfcf 10.05:1
--mark-graft #9f4325 6.09:1 #f99b7e 9.14:1

Components use semantic tokens only. Each names a job, not a value, and each theme gives it a value. There are six.

TokenLightDark
--surface #fbfaf7 1.00:1 #100f0d 1.00:1
--surface-raised #f2f2ee 1.08:1 #1a1917 1.09:1
--border #e5e5e1 1.21:1 #2e2e2b 1.41:1
--border-strong #81807d 3.78:1 #6c6c69 3.64:1
--text #191816 17.00:1 #e8e8e5 15.60:1
--text-muted #64635f 5.76:1 #a5a5a1 7.75:1
  • surface is the page; surface-raised is code and editors.
  • border separates; border-strong outlines a control, and clears 3:1.
  • text is almost everything; text-muted is supporting text, and clears 4.5:1.
  • A state tint, like the amber behind a blocked item’s “waits on”, is mixed from that state’s mark with color-mix, so no new colour enters the palette.

Two families, both from the system: the platform’s sans for everything, and its monospace for code and marks. No font files are shipped.

TokenSizeLine heightSample
--text-xs0.75rem1.45Plans
--text-sm0.875rem1.5Plans
--text-base1rem1.6Plans
--text-md1.125rem1.55Plans
--text-lg1.375rem1.35Plans
--text-xl1.75rem1.2Plans
--text-2xl2.5rem1.08Plans
--text-3xl3.5rem1.02Plans

Three weights: regular 400, medium 500, bold 650. Sizes from xl up tighten their tracking slightly.

One scale for margin, padding and gap. Sections are separated by the two largest steps, which is most of what “letting it breathe” means in practice.

TokenValue
--space-14px
--space-28px
--space-312px
--space-416px
--space-524px
--space-632px
--space-748px
--space-864px
--space-996px
--space-10128px
  • Radius: none (0), sm (4px), md (8px), full (999px).
  • Motion: 120ms and 200ms, one easing; both drop to zero under prefers-reduced-motion.
  • Layers: base, sticky, overlay. No literal z-index.
  • Breakpoints: 560px, 860px, 1180px. Media queries can’t read custom properties, so these three are the only widths a media query may use, and the build checks it.
  • Target: 44px, the smallest a control is by touch, below the 860px breakpoint or on a coarse pointer. The build checks every page at a phone’s width for a smaller one.
  • No shadows. Elevation is a change of surface, not a drop shadow.

One mark in its colour. Decorative wherever its name is shown beside it; given a label when it stands alone.

+

The nine marks as a set, in the language’s order and groups. full links each mark to its rule in the spec; compact is a legend.

  1. to do
  2. doing
  3. done
  4. dropped
  5. later
  1. question
  2. answer
  1. group
  2. graft

Do show the marks as a set only through this component. Don’t recolour a mark for emphasis, or show a subset without saying which.

Primary is the one action a section is for; there is at most one per section. Secondary is an alternative, ghost a quiet way out.

A Sprig example, highlighted by the same rules everywhere: marks in their colours, everything else neutral.

release.sprig
Release 2.0  @kim

- Migration guide  ^guide
? Keep the free tier?  ^tier
= Yes, capped at three devices.
- Announce  after:^guide  after:^tier

A plan as Sprig reads it, rendered at build time by the parser.

release.sprig, as Sprig reads it
  • Migration guide^guide
  • Keep the free tier?^tier
  • Announcewaits on Migration guidewaits on Keep the free tier?

Stack, Inline, Grid and Text do the layout, and accept only token values: gap={5} type-checks, gap="13px" doesn’t. A component never sets its own margin; the parent’s gap does the spacing. Section is the one pattern: a heading, an optional introduction and content, at the site’s rhythm.

  1. Colour is a token. scripts/lint-tokens.mjs fails the build on any colour value outside site/src/design/tokens.mjs, on spacing or type sizes off the scales, and on media queries at other widths.
  2. A component needs a real use. The site has what it uses and nothing it might one day. A pattern becomes a component the third time it appears.
  3. Changing a token is a system change. Adding one is cheap. Changing one moves every page, which is the point, so it’s done deliberately.

Every page is checked with axe in both themes on every build, and a violation fails it.