HomeProjectsNotes
Theme
Back to Notes

Adding Tailwind CSS in Turborepo

2025-12-07

Turborepo with Tailwind CSS setup diagram

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-react moved from 0.x to 1.x and typescript moved from 5.x to 7.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

  1. Project Structure Overview
  2. Setting Up the UI Package
  3. Configuring Tailwind CSS
  4. Creating Reusable Components
  5. Integrating with Your Web App
  6. 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 exports field makes specific paths available to consuming apps
  • Tailwind and PostCSS are in devDependencies
  • Helper libraries like clsx and tailwind-merge are in dependencies

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 @import directive
  • 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-authority for 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>
  );
}