Skip to content

Building a Design System from Scratch for a Personal Site

3 min read

When I decided to rebuild my site, the tempting path was: pick an Astro theme, customise the colours, ship it. There are good themes. I looked at several. But none of them matched the specific combination of constraints I had:

  • 0px border radius everywhere
  • A single font for display and body
  • A specific teal accent that stays constant across dark and light modes
  • Dark-first (not system-preference adaptive)

Any pre-built theme would require more fighting than building. So I built from scratch.

Start with tokens, not components

The mistake I’ve seen (and made before) is to start by building components. You build a Button, then a Card, then realise you’ve hardcoded colours inconsistently and have to refactor everything.

Start with tokens.

Tokens are the single source of truth for every design decision. Before writing a single component, define:

:root {
  /* Backgrounds */
  --color-bg: #0e1415;
  --color-bg-surface: #151e1f;

  /* Text */
  --color-text: #e8edf0;
  --color-text-muted: #8da0a4;

  /* Accent — constant, not semantic */
  --color-accent: #2f636a;

  /* Borders */
  --color-border: #233235;
}

Everything in the system refers to these variables. If you need to change the accent colour globally, you change one line.

The dark/light switch problem

Most design systems define separate palettes for dark and light mode. The semantic tokens map differently in each:

:root {
  /* dark defaults */
  --color-bg: #0e1415;
  --color-text: #e8edf0;
}

[data-theme='light'] {
  --color-bg: #f4f6f7;
  --color-text: #0e1415;
}

The accent colour is the exception. I wanted #2f636a to appear identically in both modes. This means it’s a brand constant, not a semantic token — so it doesn’t get overridden in [data-theme="light"].

The Tailwind v4 integration

Tailwind v4 makes this pattern natural. You define tokens in @theme and they become both Tailwind utilities and raw CSS variables:

@theme {
  --color-accent: #2f636a;
}

Now you can use:

  • text-accent (Tailwind utility)
  • color: var(--color-accent) (raw CSS)
  • style="color: var(--color-accent)" (inline)

All three reference the same value. The system is consistent by construction.

Document as you go

The hardest part of a design system isn’t building it — it’s keeping the documentation honest. I maintain a DESIGN.md at the repo root that tracks:

  • Every token value and its semantic meaning
  • Component specifications with state descriptions
  • The decision log: what was decided, when, and why

The decision log is the most valuable part. Six months from now, I won’t remember why the accent is #2f636a and not #3a7a83. The log tells me.

When pre-built is the right call

Most projects should use a pre-built theme or component library. Vercel’s v0, shadcn/ui, DaisyUI — these are well-tested, accessible, and save significant time.

Build your own when:

  1. The constraints are specific enough that any pre-built solution requires more adaptation than building
  2. You want to deeply understand the system you’re working with
  3. The project is a personal site where the learning cost is the point

For a company product, almost always reach for established tooling. For a personal site — sometimes the friction is the value.