Setting up Tailwind CSS in a Turborepo monorepo requires careful configuration to ensure styles are shared across apps while maintaining the benefits of a monorepo architecture. This guide walks you through the complete setup process with a real-world example.
Updated 2026-08-19: Dependency versions below were refreshed to the latest releases.
lucide-reactmoved from0.xto1.xandtypescriptmoved from5.xto7.x(the new Go-based compiler) — both are major version bumps, so re-check your own codebase against their changelogs before upgrading rather than copy-pasting blindly.
Table of Contents
- Project Structure Overview
- Setting Up the UI Package
- Configuring Tailwind CSS
- Creating Reusable Components
- Integrating with Your Web App
- Complete Code Reference
Project Structure Overview
Our monorepo follows this structure:
project-root/
├── apps/
│ └── web/
│ ├── app/
│ ├── components/
│ ├── next.config.js
│ └── package.json
└── packages/
└── ui/
├── src/
│ ├── components/
│ ├── lib/
│ └── styles/
├── package.json
└── postcss.config.mjs
The key principle is centralizing Tailwind configuration in the packages/ui package, allowing all apps to import pre-configured components and styles.
Setting Up the UI Package
1. Package Configuration
First, let's configure the UI package to properly export all necessary files:
File: packages/ui/package.json
{
"name": "@repo/ui",
"version": "0.0.0",
"private": true,
"type": "module",
"exports": {
"./*": "./src/*.tsx",
"./globals.css": "./src/styles/globals.css",
"./postcss.config": "./postcss.config.mjs",
"./lib/*": "./src/lib/*.ts",
"./components/*": "./src/components/*.tsx",
"./hooks/*": "./src/hooks/*.ts"
},
"scripts": {
"lint": "eslint . --max-warnings 0"
},
"devDependencies": {
"@repo/eslint-config": "workspace:*",
"@repo/typescript-config": "workspace:*",
"@tailwindcss/postcss": "^4.3.3",
"@types/node": "^22.15.3",
"@types/react": "19.2.2",
"@types/react-dom": "19.2.2",
"eslint": "^9.39.1",
"postcss": "^8.5.26",
"tailwindcss": "^4.3.3",
"tailwindcss-animate": "^1.0.7",
"typescript": "^7.0.2"
},
"dependencies": {
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"react": "^19.2.8",
"react-dom": "^19.2.8",
"tailwind-merge": "^3.6.0"
}
}
Key Points:
- The
exportsfield makes specific paths available to consuming apps - Tailwind and PostCSS are in
devDependencies - Helper libraries like
clsxandtailwind-mergeare independencies
2. TypeScript Configuration
File: packages/ui/tsconfig.json
{
"extends": "@repo/typescript-config/react-library.json",
"compilerOptions": {
"moduleResolution": "node",
"module": "commonjs",
"baseUrl": ".",
"paths": {
"@workspace/ui/*": ["./src/*"]
}
},
"include": [".", "src"],
"exclude": ["node_modules", "dist"]
}
File: packages/ui/tsconfig.lint.json
{
"extends": "@workspace/typescript-config/react-library.json",
"compilerOptions": {
"outDir": "dist"
},
"include": ["src", "turbo"],
"exclude": ["node_modules", "dist"]
}
3. ESLint Configuration
File: packages/ui/eslint.config.mjs
import { config } from "@workspace/eslint-config/react-internal";
/** @type {import("eslint").Linter.Config} */
export default config;
Configuring Tailwind CSS
1. PostCSS Configuration
File: packages/ui/postcss.config.mjs
const config = {
plugins: ["@tailwindcss/postcss"],
};
export default config;
This minimal configuration uses Tailwind CSS v4's PostCSS plugin, which handles all the heavy lifting.
2. Global Styles with Tailwind
File: packages/ui/src/styles/globals.css
@import "tailwindcss";
@source "../../../../packages/ui/src/**/*.{js,ts,jsx,tsx}";
@custom-variant dark (&:is(.dark *));
@theme {
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
:root {
--background: hsl(0 0% 100%);
--foreground: hsl(0 0% 3.9%);
--card: hsl(0 0% 100%);
--card-foreground: hsl(0 0% 3.9%);
--popover: hsl(0 0% 100%);
--popover-foreground: hsl(0 0% 3.9%);
--primary: hsl(0 0% 9%);
--primary-foreground: hsl(0 0% 98%);
--secondary: hsl(0 0% 96.1%);
--secondary-foreground: hsl(0 0% 9%);
--muted: hsl(0 0% 96.1%);
--muted-foreground: hsl(0 0% 45.1%);
--accent: hsl(0 0% 96.1%);
--accent-foreground: hsl(0 0% 9%);
--destructive: hsl(0 84.2% 60.2%);
--destructive-foreground: hsl(0 0% 98%);
--border: hsl(0 0% 89.8%);
--input: hsl(0 0% 89.8%);
--ring: hsl(0 0% 3.9%);
--chart-1: hsl(12 76% 61%);
--chart-2: hsl(173 58% 39%);
--chart-3: hsl(197 37% 24%);
--chart-4: hsl(43 74% 66%);
--chart-5: hsl(27 87% 67%);
--radius: 0.6rem;
}
.dark {
--background: hsl(0 0% 3.9%);
--foreground: hsl(0 0% 98%);
--card: hsl(0 0% 3.9%);
--card-foreground: hsl(0 0% 98%);
--popover: hsl(0 0% 3.9%);
--popover-foreground: hsl(0 0% 98%);
--primary: hsl(0 0% 98%);
--primary-foreground: hsl(0 0% 9%);
--secondary: hsl(0 0% 14.9%);
--secondary-foreground: hsl(0 0% 98%);
--muted: hsl(0 0% 14.9%);
--muted-foreground: hsl(0 0% 63.9%);
--accent: hsl(0 0% 14.9%);
--accent-foreground: hsl(0 0% 98%);
--destructive: hsl(0 62.8% 30.6%);
--destructive-foreground: hsl(0 0% 98%);
--border: hsl(0 0% 14.9%);
--input: hsl(0 0% 14.9%);
--ring: hsl(0 0% 83.1%);
--chart-1: hsl(220 70% 50%);
--chart-2: hsl(160 60% 45%);
--chart-3: hsl(30 80% 55%);
--chart-4: hsl(280 65% 60%);
--chart-5: hsl(340 75% 55%);
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
/* https://tailwindcss.com/docs/upgrade-guide#buttons-use-the-default-cursor */
button:not(:disabled),
[role="button"]:not(:disabled) {
cursor: pointer;
}
}
Key Features:
- Uses Tailwind v4's
@importdirective - Defines custom CSS variables for theming
- Includes dark mode support
- Sets up design tokens that work with Tailwind utilities
Creating Reusable Components
1. Utility Functions
File: packages/ui/src/lib/cn.ts
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
This utility combines clsx for conditional classes and tailwind-merge to properly merge Tailwind classes without conflicts.
2. Button Component
File: packages/ui/src/components/button.tsx
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "../lib/cn";
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
fam: "hover:bg-accent hover:text-accent-foreground",
gradient:
"bg-gradient-to-r from-teal-800 to-green-600 text-white hover:from-teal-600 hover:to-green-700 shadow-lg hover:shadow-xl transform hover:scale-105",
},
size: {
default: "h-10 px-4 py-2",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, ...props }, ref) => {
return (
<button
className={cn(buttonVariants({ variant, size }), className)}
ref={ref}
{...props}
/>
);
}
);
Button.displayName = "Button";
export { Button, buttonVariants };
Component Features:
- Uses
class-variance-authorityfor type-safe variant management - Fully typed with TypeScript
- Supports multiple variants and sizes
- Follows React best practices with
forwardRef
Integrating with Your Web App
1. Next.js Configuration
File: apps/web/next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
transpilePackages: ["@repo/ui"],
};
export default nextConfig;
The transpilePackages option tells Next.js to process the UI package, which is essential for the Tailwind setup to work.
2. Package Dependencies
File: apps/web/package.json
{
"install": [
"lucide-react": "^1.33.0",
"@tailwindcss/postcss": "^4.3.3",
"postcss": "^8.5.26",
"tailwindcss": "^4.3.3",
"tailwindcss-animate": "^1.0.7"
]
}
3. PostCSS Configuration
File: apps/web/postcss.config.mjs
export { default } from "@repo/ui/postcss.config";
This re-exports the PostCSS configuration from the UI package, ensuring consistency.
4. Layout Setup
File: apps/web/app/layout.tsx
import "@repo/ui/globals.css";
Import the global styles from the UI package instead of a local CSS file. This is the key change that enables Tailwind across your app.
5. Page Implementation
File: apps/web/app/page.tsx
import Appbar from "../components/appbar";
export default function Home() {
return (
<div className="flex items-center justify-center h-screen w-screen ">
<Appbar />
</div>
);
}
6. App Component Example
File: apps/web/components/appbar.tsx
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { Button } from "@repo/ui/components/button";
import { Menu, X, Pencil } from "lucide-react";
import { cn } from "@repo/ui/lib/cn";
const navigation = [
{ name: "About", href: "#about" },
{ name: "Contact", href: "#contact" },
];
export default function Appbar() {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const router = useRouter();
const handleSignIn = () => {
router.push("/signin");
};
return (
<header className="fixed top-0 left-0 right-0 z-50 bg-white/80 backdrop-blur-lg border-b border-gray-200/50">
<nav className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-16">
{/* Logo */}
<div className="flex items-center gap-2">
<div className="w-8 h-8 bg-gradient-to-br from-teal-600 to-green-600 rounded-lg flex items-center justify-center">
<Pencil className="w-4 h-4 text-white" />
</div>
<span className="text-xl font-bold bg-gradient-to-r from-teal-600 to-green-600 bg-clip-text text-transparent">
DrawTogether
</span>
</div>
{/* Desktop Navigation */}
<div className="hidden md:flex items-center gap-8">
{navigation.map((item) => (
<a
key={item.name}
href={item.href}
className="text-gray-600 hover:text-gray-900 transition-colors font-medium"
>
{item.name}
</a>
))}
</div>
{/* Desktop CTA */}
<div className="hidden md:flex items-center gap-4">
<Button
variant="fam"
className="text-gray-600 hover:text-gray-900 border"
onClick={handleSignIn}
>
Sign In
</Button>
<Button variant="gradient" className="shadow-lg hover:shadow-xl">
Get Started
</Button>
</div>
{/* Mobile menu button */}
<div className="md:hidden">
<Button
variant="fam"
size="icon"
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
>
{mobileMenuOpen ? (
<X className="h-5 w-5" />
) : (
<Menu className="h-5 w-5" />
)}
</Button>
</div>
</div>
{/* Mobile Navigation */}
<div
className={cn(
"md:hidden overflow-hidden transition-all duration-300 ease-in-out",
mobileMenuOpen ? "max-h-64 opacity-100 pb-4" : "max-h-0 opacity-0"
)}
>
<div className="space-y-4 pt-4 border-t border-gray-200">
{navigation.map((item) => (
<a
key={item.name}
href={item.href}
className="block text-gray-600 hover:text-gray-900 transition-colors font-medium"
onClick={() => setMobileMenuOpen(false)}
>
{item.name}
</a>
))}
<div className="flex flex-col text-center gap-2 pt-4">
<Button
variant="fam"
className="justify-center border"
onClick={handleSignIn}
>
Sign In
</Button>
<Button variant="gradient">Get Started</Button>
</div>
</div>
</div>
</nav>
</header>
);
}