Using Filament v3 with Tailwind CSS v4: A pnpm Workspaces Approach

A quick note: I created this setup a several months ago so I may have missed some details in this post. There was a bit of discussion on the Filament repo about this. If you spot anything or have any questions, please let me know.

The FilamentThemes guide shows how to run Tailwind v4 alongside Filament's v3 requirements using npm aliasing. This works, but for production apps, pnpm workspaces provide better isolation and fewer conflicts.

The Problem

Filament needs Tailwind v3, but you want Tailwind v4 features on your frontend. Package aliasing works but can be fragile.

Why pnpm Workspaces?

After trying npm workspaces → yarn workspaces → pnpm workspaces, we found pnpm's isolatedModules feature provides true dependency separation and enables concurrent development without conflicts.

Setup Guide

Project Structure

project-root/
├── packages/
   ├── frontend/          # Tailwind v4 environment
      ├── package.json
      └── postcss.config.js
   └── admin/             # Tailwind v3 environment
       ├── package.json
       └── postcss.config.js
├── pnpm-workspace.yaml
├── vite.frontend.config.js
├── vite.admin.config.js
└── package.json

Setting Up Workspaces

First, configure the workspace structure:

# pnpm-workspace.yaml
packages:
    - "packages/*"
// package.json (root)
{
    "private": true,
    "type": "module",
    "workspaces": ["packages/*"],
    "pnpm": {
        "isolatedModules": true
    },
    "scripts": {
        "dev": "concurrently \"pnpm run watch:frontend\" \"pnpm run watch:admin\"",
        "build": "concurrently \"pnpm run build:frontend\" \"pnpm run build:admin\"",
        "watch:frontend": "vite build --watch --config vite.frontend.config.js",
        "watch:admin": "vite build --watch --config vite.admin.config.js"
    }
}

Frontend Package (Tailwind v4)

// packages/frontend/package.json
{
    "name": "frontend",
    "private": true,
    "type": "module",
    "dependencies": {
        "@tailwindcss/postcss": "^4.0",
        "tailwindcss": "^4.0"
        // Add any additional PostCSS plugins or Tailwind plugins as needed
    }
}
// packages/frontend/postcss.config.js
export default {
    plugins: {
        "@tailwindcss/postcss": {},
        // Add other PostCSS plugins as needed
    },
};

Admin Package (Tailwind v3)

// packages/admin/package.json
{
    "name": "admin",
    "private": true,
    "type": "module",
    "dependencies": {
        "tailwindcss": "^3.4.3",
        "@tailwindcss/forms": "^0.5.7",
        "@tailwindcss/typography": "^0.5.12",
        "autoprefixer": "^10.4.19",
        "postcss": "^8.4.38",
        "postcss-nesting": "^12.1.2"
    }
}
// packages/admin/postcss.config.js
import tailwindcss from "tailwindcss";
import tailwindcssNesting from "tailwindcss/nesting/index.js";
import autoprefixer from "autoprefixer";

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

Vite Configuration

Configure separate Vite builds that point to their respective workspace packages:

// vite.frontend.config.js
import { defineConfig } from "vite";
import laravel from "laravel-vite-plugin";
import path from "path";

const postcssPath = path.resolve("packages/frontend/postcss.config.js");
const tailwindPath = path.resolve("packages/frontend/node_modules/tailwindcss");

export default defineConfig({
    css: {
        postcss: postcssPath,
    },
    resolve: {
        alias: {
            tailwindcss: tailwindPath,
        },
    },
    plugins: [
        laravel({
            input: ["resources/css/app.css", "resources/js/app.js"],
            buildDirectory: "frontend",
            refresh: true,
        }),
    ],
});
// vite.admin.config.js
import { defineConfig } from "vite";
import laravel from "laravel-vite-plugin";
import path from "path";

const postcssPath = path.resolve("packages/admin/postcss.config.js");
const tailwindPath = path.resolve("packages/admin/node_modules/tailwindcss");

export default defineConfig({
    css: {
        postcss: postcssPath,
    },
    resolve: {
        alias: {
            tailwindcss: tailwindPath,
        },
    },
    plugins: [
        laravel({
            input: ["resources/css/filament/admin/theme.css"],
            buildDirectory: "admin",
            refresh: true,
        }),
    ],
});

CSS Files

/* resources/css/app.css - Frontend using Tailwind v4 */
@import "tailwindcss";

@layer base {
    html {
        scroll-behavior: smooth;
        color-scheme: dark light;
    }
}
/* resources/css/filament/admin/theme.css - Admin using Tailwind v3 */
@import "/vendor/filament/filament/resources/css/theme.css";

@config 'tailwind.config.js';

@tailwind base;
@tailwind components;
@tailwind utilities;

Filament Configuration

Don't forget to update your Filament panel provider to use the correct build directory:

// app/Providers/Filament/AdminPanelProvider.php
->viteTheme('resources/css/filament/admin/theme.css', 'admin')

The second parameter must match the buildDirectory in your vite.admin.config.js:

// vite.admin.config.js
laravel({
    input: ["resources/css/filament/admin/theme.css"],
    buildDirectory: "admin", // This must match!
    refresh: true,
});

Running Both Environments

Both environments can run simultaneously:

pnpm run dev  # Runs both frontend and admin builds concurrently

Frontend runs on port 3000 (Tailwind v4), admin on port 3001 (Tailwind v3).

Benefits

  • True isolation: Each environment has its own node_modules - no version conflicts possible
  • Easier debugging: When something breaks, you know exactly which environment is affected
  • Independent upgrades: Update Tailwind v4 frontend without breaking Filament admin
  • Concurrent development: Both builds run simultaneously without interference

Trade-offs

Pros: Complete isolation, easier maintenance, concurrent workflows
Cons: Slightly more setup, larger node_modules

Conclusion

While npm aliasing works for quick setups, pnpm workspaces provide a more robust foundation for production applications. The key insight: explicit separation is better than clever aliasing for applications that need to last.