Material
How a surface behaves: how sharp its corners are, and how far off the page it sits. Between them these two tokens decide whether an interface reads as precise or as friendly.
Radius
Tight, and almost never above 4px outside pills. This is the single highest-leverage token in the system — raising it to 12px would make everything read as a consumer app without a line of layout changing.
Elevation
Each step pairs a tight contact shadow with a diffuse one. A single shadow always looks pasted on, because real objects cast both a sharp shadow where they touch and a soft one further out.
Drawn edges
Every hairline in the system is roughened by an SVG filter, so rules, borders and tick marks read as drawn rather than as rendered. It is a single treatment applied system-wide, not a per-component decoration.
How the filter works
Four primitives in app/components/ui/RoughDefs.tsx. The last two are the ones that matter:
- feTurbulence — a fractal noise field.
- feDisplacementMap — pushes the source’s edges around using that noise. This is the wobble, and on its own it reads as distorted rather than drawn.
- feColorMatrix — collapses the same noise to an alpha-only ramp.
- feComposite (operator=“in”) — keeps the wobbled line only where that ramp is opaque, so the stroke varies in density and breaks up. This is the step that reads as graphite instead of a warped rectangle.
Two rules for applying it
- Never onto an element containing text. Displacement smears glyphs. Anything with both a border and a label draws its border on an
::afteroverlay, which the filter can hit alone. - Never onto an ancestor of a fixed element. A filtered element becomes the containing block for
position: fixeddescendants, which re-anchors them to it. This is why the Ruler itself is untouched and only its leaf tick layer is filtered.
Borders are kept and set to transparent rather than removed, so box metrics never change and nothing reflows when the treatment is switched off.
The dial
Set --rough-edge and --rough-edge-bold to none in tokens.css and the entire system goes crisp — no component edit, no markup change. Deleting app/styles/rough.css removes it completely.
Depth without shadow
Most separation in this system is done with a hairline, not an elevation. Shadow is reserved for things that genuinely float above the page — a card you can click, a surface that will move. Everything else earns its edge with --rule, which costs nothing and never muddies the background.