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.
One idea
Section titled “One idea”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.
Status
The marks
Section titled “The marks”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.
| Token | Light | Dark |
|---|---|---|
--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 |
Colour
Section titled “Colour”Components use semantic tokens only. Each names a job, not a value, and each theme gives it a value. There are six.
| Token | Light | Dark |
|---|---|---|
--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.
| Token | Size | Line height | Sample |
|---|---|---|---|
--text-xs | 0.75rem | 1.45 | Plans |
--text-sm | 0.875rem | 1.5 | Plans |
--text-base | 1rem | 1.6 | Plans |
--text-md | 1.125rem | 1.55 | Plans |
--text-lg | 1.375rem | 1.35 | Plans |
--text-xl | 1.75rem | 1.2 | Plans |
--text-2xl | 2.5rem | 1.08 | Plans |
--text-3xl | 3.5rem | 1.02 | Plans |
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.
| Token | Value | |
|---|---|---|
--space-1 | 4px | |
--space-2 | 8px | |
--space-3 | 12px | |
--space-4 | 16px | |
--space-5 | 24px | |
--space-6 | 32px | |
--space-7 | 48px | |
--space-8 | 64px | |
--space-9 | 96px | |
--space-10 | 128px |
Radius, motion, layers and breakpoints
Section titled “Radius, motion, layers and breakpoints”- 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.
Components
Section titled “Components”One mark in its colour. Decorative wherever its name is shown beside it; given a label when it stands alone.
+
MarkArray
Section titled “MarkArray”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.
- to do
- doing
- done
- dropped
- later
- question
- answer
- group
- 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.
Button
Section titled “Button”
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 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.
- Migration guide^guide
- Keep the free tier?^tier
- Announcewaits on Migration guidewaits on Keep the free tier?
Primitives
Section titled “Primitives”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.
- Colour is a token.
scripts/lint-tokens.mjsfails the build on any colour value outsidesite/src/design/tokens.mjs, on spacing or type sizes off the scales, and on media queries at other widths. - 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.
- 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.