
When Tailwind classes from your shared UI package don’t work in a monorepo, it’s usually because PostCSS can't find them. Here's the fix.
You set up a monorepo with Turborepo, pnpm workspaces, and a shared UI library. You import a button from your @package/ui package, spin up the dev server, and the styles are completely broken.
No errors. No warnings. The classes just don’t apply.
First, you check the obvious stuff. Is Tailwind installed? Yes. Is PostCSS configured? Yes. Is the component importing correctly? Also yes. You add a bg-red-500 to a div in your app and it works fine. Tailwind is running.
But your shared UI components remain unstyled.
At this point, you might start tweaking your @theme directives, wondering if your CSS variables are misconfigured. You refactor your color tokens. You move files around.
Before you go down that rabbit hole, check your PostCSS content paths. This should be the first port of call for mysteriously missing styles in a monorepo setup.
Tailwind v4 works by scanning your files for class names, then generating only the CSS you actually use. The key word here is “scanning.” By default, Tailwind looks for files in your project directory. Here’s what a typical PostCSS config looks like:
// postcss.config.ts
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;Clean and simple. Tailwind figures out where to look based on your project root.
The problem? In a monorepo, your UI package lives outside your app’s directory:
.
├── apps
│ ├── admin <- postcss runs here
│ └── client
├── packages
│ └── ui <- but your components live hereWhen PostCSS runs inside apps/admin, it has no idea that packages/ui even exists. Those files never get scanned, so those classes never get generated. Your styles vanish into the void.
You need to tell PostCSS two things: where the monorepo root is, and which paths to scan for Tailwind classes.
// postcss.config.ts
import path from "path";
const config = {
plugins: {
"@tailwindcss/postcss": {
base: path.resolve(process.cwd(), "../../"),
content: [
"./src/**/*.{js,ts,jsx,tsx,mdx}",
"./app/**/*.{js,ts,jsx,tsx,mdx}",
"../../packages/ui/src/**/*.{js,ts,jsx,tsx,mdx}",
],
},
},
};
export default config;The base option sets the root directory for resolving paths. The content array lists every location where Tailwind should look for class names. Now PostCSS can see your UI package, and your styles come back to life.
Important: Every app in your monorepo needs this same PostCSS configuration. Both apps/admin and apps/client need their own postcss.config.ts with the correct paths. However, you don’t need to add this config to @package/ui itself; the scanning happens on the consumer side, not the provider side. Your UI package just exports components and styles, and the apps consuming it handle the PostCSS processing.
Your @package/ui exports should work as expected once the scanning is configured. If you’re using a shared global.css that pulls in your theme, typography, color variables, and other chunks, import it in your app’s layout or entry point:
// app/layout.tsx
import "@package/ui/global.css";Here’s what a typical global.css structure might look like using Tailwind v4’s new directives:
/* packages/ui/src/global.css */
@import "tailwindcss";
@import "./theme/typography.css";
@import "./theme/colors.css";
@theme inline {
--color-surface: var(--color-primary-50);
--color-surface-elevated: white;
--font-sans: "Inter", system-ui, sans-serif;
--font-mono: "JetBrains Mono", monospace;
}
@utility btn-primary {
@apply py-2.5 px-4 bg-surface hover:bg-surface-elevated;
}All of these get processed through the same PostCSS pipeline, so your design tokens and base styles propagate across every app in the monorepo.