7 min read

Building a Themeable Nuxt Portfolio

How I built a portfolio that ships five completely different visual identities from a single set of CSS variables — and why the token layer matters more than the components.

Nuxt 4Tailwind CSSDesign Systems
Building a Themeable Nuxt Portfolio

Why five themes

Most portfolios pick one aesthetic and commit. I wanted the site itself to be the demo — terminal, blueprint, cyberpunk, editorial, and bento, each a genuinely different personality, all served from the same components.

The constraint that made it work: components never hardcode a color.

The token layer

Every theme is a block of CSS custom properties. Components reference semantic names, never raw values.

[data-theme='terminal'] {
  --t-base: #0b0f0d;
  --t-text-1: #d6e5dc;
  --t-accent: #4ade80;
  --t-accent-2: #22d3ee;
  --t-line: #1c2b23;
}

Tailwind v4 makes this cheap — no tailwind.config.js, just @theme in the main stylesheet mapping utilities onto the variables:

@theme {
  --color-accent: var(--t-accent);
  --color-t1: var(--t-text-1);
}

Now text-t1 and bg-accent mean whatever the active theme says they mean.

What actually needed per-theme branching

Three things resisted pure tokenization:

  • Voice — the terminal theme says $ ls ~/projects, the blueprint theme says FIG. Work. Copy is identity, so a handful of v-if="theme === '...'" blocks survive.
  • Card chrome — a shared ThemeCard component switches its frame per theme.
  • Home layout — bento and editorial restructure the page enough that they get their own root components rather than a re-skin.

Everything else — every page, every button, every tag pill — is theme-agnostic.

The lesson

The temptation is to build theme variants at the component level. Don't. Push the variance down into tokens and let components stay boring. When I added the fifth theme it took under an hour, because there was nothing to change but a variable block and a few strings.