·9 min read·

Rebuilding our design system on Nuxt UI 3 and Tailwind 4.

We've been on Tailwind since v0.x. We've been on Nuxt UI since the early days. The 2025 jump to v3 / v4 was bigger than expected — and worth it. Here is the migration walk-through, the @theme approach, and the patterns we ended up with.

Tailwind 4 dropped the JavaScript config in favour of @theme. Nuxt UI 3 swung over with it. We were a major version behind on both and decided to take the upgrade as an excuse to redo our shared component library properly. Three weeks of work later, the result is the cleanest design system we have ever shipped.

The headline shift — config moves into CSS

For ten years Tailwind shipped a tailwind.config.js file where you'd extend the theme. In v4 that's gone. Design tokens now live in CSS, declared with the @theme directive. Tailwind generates utility classes from those tokens at build time.

assets/css/main.csscss
@import "tailwindcss";
@import "@nuxt/ui";

@theme {
  /* Brand */
  --color-brand-orange: #f15f22;
  --color-brand-orange-soft: #ff7a3c;
  --color-brand-orange-dark: #b93f12;

  /* Canvas */
  --color-canvas-night: #000000;
  --color-canvas-night-soft: #0a0a0a;
  --color-canvas-charcoal: #111113;

  /* Hairlines */
  --color-hairline-on-dark: #343438;

  /* Type */
  --color-on-primary: #ffffff;
  --color-on-primary-mute: #d7d7dc;
  --color-on-primary-subtle: #8d8d94;

  /* Fonts */
  --font-display: "Anton", Arial, sans-serif;
  --font-sans: "Inter", Arial, sans-serif;
  --font-mono: "JetBrains Mono", monospace;

  /* Radii */
  --radius-xs: 4px;
  --radius-pill: 32px;
}

What got better

  • 01CSS variables as the single source of truth — actual design tokens, not Tailwind config impersonating them.
  • 02Build times dropped sharply. Tailwind 4 is fast — full project builds in under a second.
  • 03Component theming in Nuxt UI 3 finally feels like a coherent API rather than a series of overrides.
  • 04Auto-discovery of all CSS files in the project — no more manual content array maintenance.
  • 05@theme tokens are usable everywhere — utility classes, CSS files, inline styles, design tools.
  • 06Variant API in Nuxt UI 3 lets us define button-style presets that look properly built-in, not hacked on.

What hurt

  • 01Every custom Tailwind plugin had to be rewritten.
  • 02Our existing @apply usage needed an audit — Tailwind 4 has opinions.
  • 03Nuxt UI variant slot APIs moved — our buttons all needed updating.
  • 04A few PostCSS plugins we depended on were not compatible and had to be replaced.
  • 05The Tailwind docs in their early v4 form were patchy. Solved within months but painful at the time.

Nuxt UI 3 button preset — the new API

app.config.tstypescript
export default defineAppConfig({
  ui: {
    button: {
      slots: {
        base: 'type-button-cap inline-flex items-center gap-2 transition rounded-[2px]'
      },
      variants: {
        variant: {
          'solid-white':
            'bg-white text-black border border-white px-7 py-4 hover:bg-transparent hover:text-white',
          'ghost-on-dark':
            'bg-transparent text-white border border-white/80 px-7 py-4 hover:bg-white hover:text-black',
          'accent-solid':
            'bg-[color:var(--color-brand-orange)] text-black border border-[color:var(--color-brand-orange)] px-7 py-4 hover:bg-[color:var(--color-brand-orange-soft)]'
        }
      }
    }
  }
})

Took us three weeks. We'd do it again. The internal library now feels like one system, not a series of patches on top of an old one.

Migration playbook for teams still on v3 / Nuxt UI 2

  • 01Map your existing tailwind.config.js theme extensions to CSS @theme custom properties.
  • 02Audit @apply usage — anywhere @apply was hiding complexity, consider unrolling it.
  • 03Rewrite custom plugins as plain CSS where possible. Most of them turn out not to need plugin status.
  • 04Adopt Nuxt UI 3's variant API for repeated button / badge / input styles.
  • 05Run the codemods Tailwind ship for config migration — they catch most of the syntax shifts.
  • 06Test in a real production scale project before committing. The whole conversion takes a fortnight; the testing takes another.

Three weeks of focused work for a year of dividends. The library now feels like one system, not a series of patches on top of an old one — and the brand tokens it expresses are the same tokens our designers see in Figma. That coherence is the actual win.

Talk to Remiam about a system like this.