design.
What this site is made of: the mark, the tokens, the type, the chips and the rules, drawn by the code that draws every other page.
The mark
My initials on a 16-unit grid, one stroke weight, every diagonal at the same angle. Nothing in it touches anything: the crossbar sits half a unit off the legs and the M sits half a unit off the A. That is the left rail of every page carried into a glyph, a monogram held together by proximity rather than by contact.
It is also what makes the A separable, which is what the tab strip needs. At 16 by 16 the lockup has no fully inked pixel in it at all. Its strokes land at 0.81 device pixels and peak at 87% ink, against eighteen solid pixels for the A on its own. So the page and the social card get both letters and the favicon gets one, and the A lifts out whole rather than as a fragment.
64
32
16
Those three are the real file, at the three sizes a browser asks for it. Its colours are pinned rather than inherited, and they answer to your browser's own light or dark setting rather than to the theme button in the footer: this glyph sits in the tab strip, not on the page, so it should match the chrome around it.
Tokens
Every colour on the site is one of these. If I typed a hex outside the stylesheets, I broke something. Light is the base, so it is what a page is with no attribute, no media query and no scripting; dark is written out twice, once for a reader following their OS and once for a reader who pinned it, and a test parses both blocks and fails if they drift apart.
The swatches are painted from the tokens and the values beside them are read back out of the live stylesheet. Press the theme button in the footer and both halves move, because neither half is written down here. They are the shipped strings rather than the ones I typed, so the build's minifier has been at them: it shortens a six-digit hex to three and folds an alpha into the hex where it can. Same colours, one step further along.
- --bg----the page
- --fg----body ink, and the mark
- --muted----prose, and every paragraph on this page
- --faint----metadata: dates, counts, labels
- --hairline----every border on the site
- --hairline-strong----a hovered row border, a swatch edge
- --outline----the 1px inset on imagery
- --dot----the dotted grid behind a framed screenshot
The accent is one hue at two lightnesses. The hue is identical in both themes and lightness is the only difference, because no single lightness clears 4.5:1 against both a white page and a near-black one.
- --accent----ink only: links, hover, aria-current, focus
- --accent-panel----the accent as a background with text on it
- --accent-panel-ink----that text
The moment the accent becomes a background with text on it, it switches to the panel pair. White on the dark theme's accent is 3.89:1 and fails AA; white on this blue is 5.91:1 in either theme, which is why this block is the same blue whichever theme you are reading it in.
And a real palette row, the component that sits under every screenshot in the gallery. Every swatch is a button, because the point of the row is that you can take the colour out of it. These six were read off the Design Engineer Tools capture by the pipeline, so they are a fact about the screenshot rather than an opinion about the site.
Type
Geist Sans for prose, Geist Mono for labels, display and metadata. Both are variable fonts bundled into the build and served from this domain, so reading a page here does not tell a font CDN that you were reading it.
- page-title
- Aa
- page-title--entry
- Paperclip
- page-title--mono
- design.
- standfirst
- One line under every title, and never two.
- mono
- Section head
The masthead at the top of this page is the mono variant and the line under it is the standfirst, so two of those five are already in front of you at full size, in the elements they normally ship as. The mono voice is 11px, weight 500, wide tracking, uppercase, and it is the site's label voice: section heads, field labels, dates, footer items, key caps.
Two rules are global and are not in the table because nothing sets them: every heading balances its line breaks and every paragraph gets the prettier ones. Both are declared once, for the elements, and no component redeclares them.
Four places undo the uppercase and each says why in place. A file path is not a label. A hex is a value you are about to paste, so it has to read exactly as it will arrive. A key cap is printed esc on the keyboard you are looking at, so shouting it would name a different key. And a token name on this page is code for the same reason a hex is.
Tabular numbers are not optional. Any digit that changes in place, or sits in a column, gets them. Here is the same clock twice, the second one without.
- tabular-nums
- --:--:--
- default
- --:--:--
Watch the right edge of the second one. Every time a digit changes width the whole string re-lays-out, which is a twitch in the corner of your eye on a page with forty dated rows on it.
Chips
A soft tint, never a solid fill, so twenty rows read as one page rather than as a traffic light. A tone sets three numbers and the tint, the hairline and the ink all derive from them, which is how light and dark stay in step without a second palette.
Every ink clears 6:1 over its own tint. Not over the page behind it: over the colour the tint actually composites to, which is the lower of the two numbers and the one you are subject to. Measuring against the page flatters every tone by about eight tenths of a point.
- verdict
- using
- watching
- on-hold
- skipped
- status
- running
- paused
- shipped
- killed
- kind
- article
- post
- video
- chip--link
Those are every verdict on /tools, every status on /experiments and every kind on /library, iterated from the same lists those pages read. A fifth verdict will show up here on the commit that adds it, and it will not compile until it has a tone.
The last row is the filter form. Same tone, same size, and the only things added are the affordances a label has no use for: a pointer cursor, and a tint that deepens on hover instead of changing colour. Hover one. That state holds AA rather than 6:1, which is the one place the vocabulary spends contrast on purpose.
Blue is the accent hue held back from the accent lightness, because an inline link and a shipped chip must not be the same blue on the same page. It is held back by two dials in the light theme and by lightness alone in the dark one, where nothing darker survives the tint.
Links
A link inside a sentence is bold and underlined, like this one. Body text here is set at 75% ink, so a blue word at weight 400 in a muted paragraph is a colour difference and nothing else, and that is the one signal that goes missing on a bad screen, in bright sun, or for a reader who cannot separate those two hues at all. Weight and a line are two more, and neither depends on seeing colour.
A link that leaves the site takes an arrow, and the arrow comes from CSS rather than from markup: rareui.com. Typing the character by hand is how a convention ends up on nine links and missing from the tenth, and how the arrow ends up inside the text a screen reader has to read out. The generated one has an empty accessible name, so it announces the link and not "north east arrow" after it.
Everything else is exempt, and exempt structurally rather than by an override. Nav dashes, chips, footer items, index rows, tool and library rows, tabs, crumbs and metadata strips are simply not in that rule, and each sets its own text-decoration: none. Bolding and underlining them would turn navigation into a paragraph of shouting.
Furniture that still has to read as a link uses the quiet pattern instead: it keeps the ink it already had and carries an underline in the hairline colour, going accent on hover. It looks like this. It carries the underline permanently rather than revealing one, because on a touchscreen there is no hover to reveal it with. It is a rule rather than a shared class, so this one specimen is drawn rather than rendered, and the three surfaces that use it each keep their own colour and alignment.
Interactions
Everything presses. The default is a 4% squeeze over 120ms, and wide elements dial it back rather than shrinking by 4%, because a full-width row losing 4% of its width reads as a glitch and not as a press. Hold each of these down.
They do nothing else. A control that cannot work is not shown on this site, so these three exist to be pressed.
Every transition names its properties. There is no transition: all anywhere, without exception, and an element that already presses does not get a second transition declaration, because the two race depending on stylesheet order.
Reduced motion flattens every animation and transition to almost nothing, zeroes delays, kills smooth scrolling and disables the press scale. What the global brake cannot do is two things, so components handle both: anything that rests on a transform restates that transform under reduced motion, and a staggered entrance is gated on the preference rather than shortened, because a shortened delay still leaves a card sitting invisible and then popping in.
The command palette opens on Cmd or Ctrl K from anywhere. Arrows move with wrap, Home and End jump, Enter follows, Escape closes, and Tab is swallowed: the field is the only focusable control in an open palette, so swallowing Tab is the focus trap. Focus never leaves the input, and the highlighted row is a virtual cursor rather than a focused element. Three surfaces listen for keys on the document and none of them may depend on running first, so every other one asks the document whether a palette is open before acting.
Scroll chaining is contained in modals and nowhere else. Containing it on a page whose main content is itself a scroller is a trap: you reach the bottom of the picture, keep scrolling to read the notes under it, and nothing happens until you physically move the pointer off the frame.
Focus is a 2px accent ring at a 3px offset. It flips to white on the blue panel, widens to a 12px offset on a 1px nav dash, and on the screenshot scroller the frame itself is the ring. Every hit area clears 40px, most of them through a pseudo-element, so a control with a toolbar's target does not put a toolbar's height into the row it sits in.
The rule
If the code and the contract disagree, fix whichever is wrong, in the same commit.
The contract is design.md, in the repository. Every claim in it names the file and the selector that enforces it, so any of it can be checked with one grep, and this page is the half of it you can press.