Design System & Branding

How @nuxfire/tokens centralizes theming across all three frontend apps, and how to rebrand the starter kit for your own product.

Every design token — brand colors, corner radius, dark-mode surface mapping, and Nuxt UI component overrides — lives in one workspace package: @nuxfire/tokens, consumed by apps/app, apps/web, and apps/login alike. Before this package existed, each app duplicated its own app.config.ts and main.css — a real risk of one app quietly drifting from the others. Rebranding now means editing one file, not three.

The package

packages/tokens/
├── theme.css   # Tailwind @theme tokens, dark-mode mapping, corner radius
└── index.ts    # sharedUiConfig (Nuxt UI app.config.ts fragment) + enterpriseTableUi

A consuming app wires it up in three places:

package.json
{ "dependencies": { "@nuxfire/tokens": "workspace:*" } }
app/assets/css/main.css
@import "tailwindcss";
@import "@nuxt/ui";
@import "@nuxfire/tokens/theme.css"; /* after @nuxt/ui, so it wins over its defaults */
app/app.config.ts
import { sharedUiConfig } from "@nuxfire/tokens";

export default defineAppConfig({
  ui: { ...sharedUiConfig }, // app-specific keys can follow the spread
});

The @source gotcha

The package reaches each app through a node_modules symlink (a Bun workspace), and Tailwind does not scan node_modules for class names by default. Without the line below, any class that exists only inside index.ts (for example, a compoundVariants override like before:bg-primary/15) renders in the DOM with no CSS rule behind it — silently broken, no error. theme.css already carries the fix:

@source "./index.ts";

If index.ts ever grows a genuinely new utility class — not one already used elsewhere in an app's own scanned source — this directive is what keeps it from going dead again.

Brand accent — shared across all three apps

flame (red, primary) and fire (orange, secondary) are defined once in theme.css, byte-identical in every app. ui.colors maps primary: 'flame', secondary: 'fire' in sharedUiConfig.

Neutral scale — intentionally different per app

Unlike the brand accent, the neutral surface scale (night-*) is not shared — each app defines its own in its own main.css, same token names, different hues:

  • apps/app uses a cold true-gray scale, matching an enterprise reference panel (Linear-style) byte-for-byte — this is the working dashboard and the platform-admin console, where a neutral base and the brand accent reserved for actions/highlights is the standard SaaS-utility convention (GitHub, Linear, Vercel all do the same).
  • apps/web and apps/login use a warm, ember-tinted gray — the public brand site's own identity.

Rebranding one app's base tone never touches the other two — edit only that app's --color-night-* block.

Corner radius — one token drives everything

:root {
  --ui-radius: 0.1875rem; /* 3px */
}

Nuxt UI derives its entire rounded-* scale proportionally from this single variable (--radius-lg: calc(var(--ui-radius) * 2), and so on), so every card, button, input, modal, and badge tightens together from one edit — no per-component override needed.

Shadow policy

Static surfaces (cards, sections, panels) use zero shadow — separation comes from a 1px border/ring, never box-shadow. Floating overlays (modals, dropdowns, popovers, tooltips) keep their shadow: they float over content with no backdrop scrim, so the shadow is the only depth cue they have — that's Nuxt UI's own default behavior, left untouched.

Component overrides (sharedUiConfig)

ComponentOverrideWhy
UCardbg-elevated instead of bg-defaultThe stock variant leaves a card the same color as the page (1:1 contrast, measured) — bg-elevated is a raised tone.
UAvatar (neutral, no image)bg-accented instead of bg-elevatedThe stock fallback shares the card's own background, so a photoless avatar inside a card had no visible background at all.
UNavigationMenu (vertical)24px icons (was 20px), collapsed rows as a centered size-11 square, active item gets a colored ring in addition to the tintA defined square tap target and a bordered active state, not just a floating glyph and a flat color wash.
UTabsdefault link variant instead of pillA solid brand-color block reads heavier than the underline treatment used across settings screens.
UTable (enterpriseTableUi)small, muted, uppercase, letter-spaced headerMatches the column-label convention Linear/Stripe/Notion tables use, instead of a heading-sized header.

enterpriseTableUi ships separately from sharedUiConfig, passed explicitly via each <UTable>'s :ui prop — an instance prop always wins, which a global slot override for this particular component had not been verified to do.

Accessibility — measured, not estimated

The dark/light surface mapping in theme.css comes from real contrast measurements against the running app (WCAG relative-luminance formula), not a guess. The stock Nuxt UI mapping measured a card at 1.00:1 against its own page background (invisible) and inactive icons at 2.61:1 (below the 4.5:1 WCAG AA floor). The current dark-mode text ladder against the #0d0d0d canvas: 18.7:1 / 15.6:1 / 12.1:1 / 5.48:1 — every tier clears AA, with dimmed deliberately the tightest since it also drives placeholders.

Rebranding checklist

  • Change the accent color everywhere: edit the flame-*/fire-* scales in packages/tokens/theme.css.
  • Change the corner radius everywhere: edit --ui-radius in the same file.
  • Change a component's default look everywhere: edit sharedUiConfig/enterpriseTableUi in packages/tokens/index.ts.
  • Change one app's neutral tone only: edit that app's own --color-night-* scale in its own main.css — not in packages/tokens.
  • Override something for one app only: spread sharedUiConfig in that app's app.config.ts, then add keys after the spread — the later definition wins.

Every token is a CSS custom property, never a class hardcoded into a component, which is also the right foundation for a future per-tenant theme editor: overriding a handful of --ui-*/--color-* variables at runtime reaches every component for free.

Nuxfire Production Kit

Ready to build and launch your SaaS?

Get 100% full source code ownership, zero proprietary wrappers, and architecture engineered for millions of requests on Cloudflare.

© 2026 Nuxfire