Skip to main content

Tailwind CSS v4 Monorepo

How do you distribute a design system across an Nx monorepo without styles breaking at the boundary?

Tailwind v4 replaces tailwind.config.js with CSS-first configuration. No more JavaScript presets, no createGlobPatternsForDependencies, no content arrays. Everything lives in CSS via @theme, @source, and @import. This simplifies monorepo architecture but changes every assumption from v3.

For token architecture (what the design system looks like), see Tailwind CSS v4 Theme. This page covers how that system distributes across an Nx monorepo.

Monorepo Architecture

The Design System Library

The shared preset lives entirely in CSS. No JavaScript config files.

libs/app-client/design-system/theme/src/styles/
tokens.css <- Raw CSS variables (:root, [data-theme])
tailwind-bridge.css <- @theme and @theme inline (generates utilities)
globals.css <- Base resets, scrollbar styles
animations.css <- @keyframes within @theme

tailwind-bridge.css is the v4 equivalent of tailwind.preset.js:

@theme {
/* Static tokens — don't change between themes */
--font-sans: "Inter", "Helvetica Now", sans-serif;
--font-mono: "JetBrains Mono", monospace;
--spacing: 0.25rem;
}

@theme inline {
/* Dynamic tokens — respond to theme changes */
--color-background: var(--app-bg);
--color-foreground: var(--app-fg);
--color-primary: var(--app-primary);
}

The App Entry Point

Each consuming app imports the design system and declares @source directives to scan shared libraries:

/* apps/dreamineering/drmg-marketing/src/app/global.css */
@import 'tailwindcss';

/* 1. Raw CSS variables (SSOT) */
@import '../../../../../libs/app-client/design-system/theme/src/styles/tokens.css';

/* 2. Base styles (resets, scrollbars) */
@import '../../../../../libs/app-client/design-system/theme/src/styles/globals.css';

/* 3. The @theme bridge (Tailwind preset) */
@import '../../../../../libs/app-client/design-system/theme/src/styles/tailwind-bridge.css';

/* 4. Scan shared libraries for class names */
@source "../../../../../libs/app-client/**/*.{js,ts,jsx,tsx,mdx}";
@source "../../../../../libs/app-server/ui-server-components/**/*.{js,ts,jsx,tsx,mdx}";

What Changed from v3

v3 Patternv4 ReplacementWhy
tailwind.config.jsCSS @theme directiveNo JS build step for config
tailwind.preset.jsShared CSS file with @themeImport via @import, not require()
content: [...] array@source directive in CSSScanner config lives where styles live
createGlobPatternsForDependencies()Manual @source pathsExplicit is better than implicit
@tailwind base/components/utilities@import 'tailwindcss'Single import replaces three directives
darkMode: 'class'@custom-variant dark (...)CSS-native variant definition

Source Scanning

The @source directive tells Tailwind's compiler where to look for class names. In a monorepo, this is the boundary between "styles that exist" and "styles that get generated."

/* Scan all client-side UI libraries */
@source "../../../../../libs/app-client/**/*.{js,ts,jsx,tsx,mdx}";

/* Scan server components that render HTML */
@source "../../../../../libs/app-server/ui-server-components/**/*.{js,ts,jsx,tsx,mdx}";

Critical rule: The path is relative to the CSS file containing the @source directive. Get this wrong and Tailwind silently generates no utilities for your shared components.

Automatic scanning: Tailwind v4 automatically scans files in the same directory tree as the CSS entry point. You only need explicit @source for files outside that tree — which is always the case for monorepo libraries.

Import Chain

The import order matters. Each layer builds on the previous:

@import 'tailwindcss'          <- Tailwind's base, components, utilities

@import tokens.css <- Raw CSS variables (:root, [data-theme])

@import globals.css <- Element resets using those variables

@import tailwind-bridge.css <- @theme maps variables to Tailwind utilities

@source paths <- Tell scanner where to find class usage

If the bridge imports before the raw variables, var() references resolve to nothing. If @source paths are wrong, shared component classes get purged.

PostCSS Setup

Tailwind v4 uses @tailwindcss/postcss instead of the old tailwindcss PostCSS plugin:

// postcss.config.js (in the app root)
module.exports = {
plugins: {
'@tailwindcss/postcss': {},
},
};

For Vite-based apps, use the Vite plugin instead:

// vite.config.ts
import tailwindcss from '@tailwindcss/vite';

export default {
plugins: [tailwindcss()],
};

Class Name Patterns

Tailwind v4's scanner is static — it cannot detect dynamically constructed class names.

// BAD — scanner cannot detect these
className={`text-${color}-500`}
className={`bg-${variant}`}

// GOOD — full class names, scanner finds them
className={cn(
variant === 'primary' && 'bg-primary text-primary-foreground',
variant === 'destructive' && 'bg-destructive text-destructive-foreground',
)}

Use cn() (from clsx + tailwind-merge) and cva() (class-variance-authority) for variant mapping. Both produce full class name strings the scanner can find.

Debugging Checklist

When styles fail to apply in a consuming app, work through this sequence:

#CheckHow
1Source pathsVerify @source relative path from the CSS file's location. Print pwd from the CSS file's directory and trace
2Import orderConfirm @import 'tailwindcss' comes first, then tokens, then bridge
3PostCSS configConfirm @tailwindcss/postcss is in the app's postcss.config.js
4Variable resolutionDevTools Computed tab: check --app-* variables resolve on :root
5Token mappingDevTools: check bg-primary resolves to a real color, not transparent
6CacheRun npx nx reset to clear Nx and Tailwind daemon caches
7Dynamic classesGrep for template literal class names — replace with cn() / cva()
8Browser supportSafari 16.4+, Chrome 111+, Firefox 128+ required for v4 features

The most common failure: @source path is wrong, so Tailwind's scanner never sees shared component classes. Styles exist in the design system but never get generated in the app's CSS output.

Rendering Verification

"Present in the DOM" is not "visible to a human." After any token or theme change, run the 5-check verification:

  1. Tokens resolve (no transparent on visible elements)
  2. Text visible (color differs from background)
  3. Images render (width and height > 0)
  4. No overflow (zero horizontal scrollbar)
  5. Fonts loaded (computed font matches intended)

See Design Verification lesson for the full audit script.

UI Libraries

DesignedHeadless
Catalyst UIHeadless UI
shadcn/uiRadix UI
Daisy UI

Context

Resources