eratic / docs / 02-design-system.md
863 words

The design system

A deliberately small one. Its job is to be a good default that never has to be fought, and to get out of the way completely when an article wants to be its own thing.

The one idea

Every value the design depends on is a CSS custom property in packages/core/src/styles/tokens.css, and nothing else hard-codes a colour, size or typeface. That is what makes per-article theming a frontmatter block rather than a fork.

Typography

Prose Newsreader Variable — weight + optical size axes
Interface Inter Variable
Mono JetBrains Mono Variable

Self-hosted via @fontsource-variable npm packages, bundled into the build and served from your own domain. No request ever leaves the reader's browser for a font, which is a privacy property as much as a performance one.

The optical size axis is why Newsreader is here: the same file renders correctly weighted at 17 px body and 56 px display, rather than looking like inflated body text. It comes from opsz.css, not index.css — see the comment in fonts.css before changing fonts.

The scale is fluid between 380 px and 1280 px viewports, opening from a minor third at body sizes to a perfect fourth at display sizes: body copy stays calm while headings gain presence on wide screens.

Prose uses old-style numerals in running text and lining numerals in headings, so a date mid-sentence does not shout.

Changing the typefaces

npm i @fontsource-variable/fraunces

Then edit two files: the import in packages/core/src/styles/fonts.css and the --font-prose stack in tokens.css. Nothing else refers to a font by name.

Colour

OKLCH, so lightness steps are perceptually even and the dark theme is a real remapping rather than an inversion. The surface is very slightly warm, the ink very slightly cool — that small temperature separation is what stops a monochrome page reading as flat grey.

Light and dark come from light-dark() and follow the reader's system setting. There is no theme toggle, on purpose: a toggle needs JavaScript and a stored preference, and the operating system already knows the answer.

prefers-reduced-motion and prefers-contrast: more are both honoured.

Layout

One grid, .canvas, with named columns. Anything can claim a wider track without a wrapper — which matters inside MDX, where you cannot add markup around a paragraph without polluting the text.

Track Width For
content 34 rem prose
wide 52 rem figures, photo grids, code
full edge to edge whatever needs it

Vertical rhythm comes from .prose > * + *, so a component dropped into the flow spaces itself correctly without knowing anything about the system.

Per-article overrides

theme

Re-declare any token for one entry:

---
title: A piece that wants to be orange
theme:
  accent: 'oklch(64% 0.19 42)'
  font-prose: "'Fraunces Variable', Georgia, serif"
  measure: '40rem'
  surface: 'oklch(97% 0.02 85)'
  leading-prose: '1.75'
---

Keys are written without the leading --. They are applied as inline custom properties on <html>, so the whole page re-tunes coherently — links, rules, captions and hover states all follow the new accent. Keys that are not plausible custom-property names are dropped rather than interpolated, so a typo cannot inject CSS.

canvas

Scoped styles and components

Inside .mdx, a <style> block is scoped to that article. For anything larger, build a component next to the entry and import it — an explicit import takes precedence over the always-available ones.

Interactive elements work, with one constraint: inline <script> is blocked by the Content-Security-Policy. Astro bundles <script> tags into external files, so use those. This is deliberate — it is the same rule that stops a third-party tracker from ever being added by accident.

Components

All take width="content" | "wide" | "full".

<Figure> One photograph. exif renders the camera settings; priority disables lazy loading for an above-the-fold image.
<PhotoGrid> A set. columns={1\|2\|3}, collapses to one on a phone. Intrinsically sized, so tall and wide frames keep their proportions.
<Video> Resolves through the media layer — see docs/05-media.md. ambient for a silent loop with no controls.
<Bleed> Break arbitrary content out of the reading column.
<Aside> A margin note on wide screens; folds into the flow as an indented note below 64 rem.

What is deliberately absent

No utility classes, no component library, no CSS framework, no build-time design token pipeline. Four sites and one author do not need an abstraction layer between intent and CSS — and every layer added here is a layer to fight when an article wants to look different.

static mirror of HEAD · about · clone: git clone https://git.makefuture.ro/eratic.git