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:
- The constraints are specific enough that any pre-built solution requires more adaptation than building
- You want to deeply understand the system you’re working with
- 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.