diff --git a/docs/blog/2025-06-19-setup-tailwind4-npm-workspace.md b/docs/blog/2025-06-19-setup-tailwind4-npm-workspace.md new file mode 100644 index 0000000000..20e10bf412 --- /dev/null +++ b/docs/blog/2025-06-19-setup-tailwind4-npm-workspace.md @@ -0,0 +1,238 @@ +--- +title: 'Configure Tailwind 4 with Vite in an NPM Workspace: The Complete Guide' +slug: setup-tailwind-4-npm-workspace +authors: ['Juri Strumpflohner'] +tags: ['nx', 'tailwind', 'vite', 'npm-workspaces', 'sync-generators'] +cover_image: /blog/images/articles/bg-tailwind-4-guide.avif +description: 'Learn how to set up Tailwind CSS v4 with Vite in an NPM workspace monorepo, and automate your configuration with Nx Sync Generators for optimal performance.' +youtubeUrl: https://youtu.be/tg3LnqhNNws +--- + +Tailwind CSS v4 brings revolutionary changes to how we configure and use the popular utility-first framework. The simplified setup eliminates configuration files and complex PostCSS setups - you just install, import, and start building. But when working in NPM workspaces or monorepos, there's still one crucial challenge: **how do you tell Tailwind which packages to scan for classes?** + +This guide walks you through setting up Tailwind v4 with Vite in an NPM workspace, then shows you how to automate the configuration using Nx Sync Generators to **eliminate manual maintenance**. + +{% github-repository url="https://github.com/juristr/tailwind4-vite-npm-workspaces" /%} + +{% toc /%} + +## Setting up Tailwind v4 + +[Tailwind v4](https://tailwindcss.com/blog/tailwindcss-v4) introduced some nice simplifications when it comes to configuring Tailwind: + +- **No more `tailwind.config.js`** - The framework works out of the box +- **Minimal dependencies** - Just `tailwindcss` and `@tailwindcss/vite` for Vite projects +- **Simple CSS import** - Add `@import "tailwindcss"` to your stylesheet and you're ready + +Since we're using Vite in this workspace, we can leverage the dedicated Tailwind Vite plugin instead of PostCSS configuration. Here's what you need: + +Install the required packages at your workspace root: + +```json {% fileName="package.json" %} +{ + "devDependencies": { + "tailwindcss": "^4.0.0", + "@tailwindcss/vite": "^4.0.0" + } +} +``` + +Configure your Vite setup: + +```typescript {% fileName="apps/shop/vite.config.ts" %} +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import tailwindcss from '@tailwindcss/vite'; + +export default defineConfig({ + plugins: [react(), tailwindcss()], + // ... rest of your config +}); +``` + +Add the import to your main CSS file: + +```css {% fileName="apps/shop/src/styles.css" %} +@import 'tailwindcss'; +``` + +## The NPM workspace challenge + +Consider a typical e-commerce application structured as an NPM workspace: + +```plain +apps/ + shop/ + src <<<< where tailwind is configured +packages/ + products/ + feat-product-list/ + feat-product-detail/ + data-access-products/ + shared/ + ui/ + utils/ +``` + +At this point, your application will build and serve, but you'll notice that styles from your `packages/` are missing. In this modular setup, your main application (`shop`) depends on various feature packages, but **Tailwind only scans the main app by default**. This means styles defined in your packages won't be included in the final bundle, leading to missing styles and broken layouts. + +## Solving the scanning problem with @source directives + +Tailwind v4 introduces the `@source` directive to address exactly this problem. You can explicitly tell Tailwind which directories to scan by adding these directives to your CSS file: + +```css {% fileName="apps/shop/src/styles.css" %} +@import 'tailwindcss'; + +@source "../../../packages/products/feat-product-list"; +@source "../../../packages/products/feat-product-detail"; +@source "../../../packages/shared/ui"; +... +``` + +With these directives in place, Tailwind will scan the specified packages and include any utility classes found there. Your application styles will now work correctly across all packages. + +## Automating @source entries - enter Nx sync generators + +While `@source` directives solve the technical problem, they introduce a maintenance challenge: + +- **manual updates required** when adding or removing dependencies, +- **easy to forget updating** the directives, +- **hard-to-debug issues** since missing styles don't break builds (just cause visual problems), and +- **team coordination** since every developer needs to remember to update these paths. + +This is where automation becomes crucial and where Nx can help. [Nx Sync Generators](/concepts/sync-generators) provide a powerful solution for **automating configuration that needs to stay in sync with your project structure**. + +For our specific use case we can automate the generation of the `@source` directives by + +- analyzing and traversing all of the `shop` application's dependencies (leveraging the [Nx project graph](/features/explore-graph)) +- generating the `@source` entries into the correct `styles.css` file + +You can follow [the guide on the Nx docs](/extending-nx/recipes/create-sync-generator) for all the details on how to implement your own Nx sync generator. At a high level these are the steps you'll need: + +**Step 1: Add Nx Plugin development support** + +```shell +npx nx add @nx/plugin +``` + +**Step 2: Generate a new plugin into your workspace** + +```shell +npx nx g @nx/plugin:plugin tools/tailwind-sync-plugin +``` + +Note, you can choose whatever folder you like. I happen to use the `tools/` folder for this example. + +**Step 3: Generate a sync generator** + +```shell +npx nx g @nx/plugin:generator --name=update-tailwind-globs --path=tools/tailwind-sync-plugin/src/generators/update-tailwind-globs +``` + +With that you have the infrastructure in place and we can look at the actual implementation of the sync generator: + +```typescript +import { Tree, createProjectGraphAsync, joinPathFragments } from '@nx/devkit'; +import { SyncGeneratorResult } from 'nx/src/utils/sync-generators'; + +export async function updateTailwindGlobsGenerator( + tree: Tree +): Promise { + const appName = '@aishop/shop'; + const projectGraph = await createProjectGraphAsync(); + + // Traverse all dependencies of the shop app + const dependencies = new Set(); + const queue = [appName]; + const visited = new Set(); + + while (queue.length > 0) { + const current = queue.shift()!; + if (visited.has(current)) continue; + visited.add(current); + + const deps = projectGraph.dependencies[current] || []; + deps.forEach((dep) => { + dependencies.add(dep.target); + queue.push(dep.target); + }); + } + + // Generate @source directives for each dependency + const sourceDirectives: string[] = []; + dependencies.forEach((dep) => { + const project = projectGraph.nodes[dep]; + if (project && project.data.root) { + const relativePath = joinPathFragments('../../../', project.data.root); + sourceDirectives.push(`@source "${relativePath}";`); + } + }); + + // Update the styles.css file + const stylesPath = 'apps/shop/src/styles.css'; + const currentContent = tree.read(stylesPath)?.toString() || ''; + + // Insert the @source directives after @import "tailwindcss" + // ... (implementation details) + + return { + outOfSyncMessage: 'Tailwind @source directives updated', + }; +} +``` + +_(Check out the [Github repo for the full implementation](https://github.com/juristr/tailwind4-vite-npm-workspaces))_ + +You can manually run sync generators with `nx sync`, but we want this to run automatically whenever we build or serve our application. As such we can register the sync generator in the app's `package.json`: + +```json {% fileName="apps/shop/package.json" %} +{ + "name": "@aishop/shop", + ... + "nx": { + "targets": { + "build": { + "syncGenerators": ["@aishop/tailwind-sync-plugin:update-tailwind-globs"] + }, + "serve": { + "syncGenerators": ["@aishop/tailwind-sync-plugin:update-tailwind-globs"] + } + } + } +} +``` + +## Nx sync generators in action + +When you run your development server with `nx serve shop`, the sync generator automatically checks if your `@source` directives are up to date: + +![Tailwind Nx sync generator in action](/blog/images/articles/tailwind-sync-generator.avif) + +Your CSS file is automatically updated with the correct directives based on your actual project dependencies. If you add or remove dependencies later, the next build or serve will **detect the changes and update the configuration automatically**. + +You can find the complete implementation in this [GitHub repository](https://github.com/juristr/tailwind4-vite-npm-workspaces). + +## Using Tailwind v3? + +If you're currently using Tailwind v3, the concept is similar but the implementation differs. Instead of updating `@source` directives, you'd modify the `tailwind.config.js` file with glob patterns. + +Check out the following video which explains the same approach for Tailwind v3: + +{% youtube src="https://www.youtube.com/watch?v=huTmV-F8c0A" title="Optimizing Tailwind with Nx Sync Generators (v3)" /%} + +The [Tailwind v3 demo repository](https://github.com/juristr/tailwind-sync-demo) shows how to implement this approach for older versions. + +## Conclusion + +While Tailwind v4's simplified setup is a significant improvement, manually maintaining `@source` directives creates a maintenance burden in monorepos. Nx Sync Generators solve this by automatically keeping your Tailwind configuration in sync with your project dependencies, eliminating manual updates and preventing hard-to-debug styling issues. + +This approach transforms configuration maintenance into a completely automated process, letting you focus on building features rather than managing paths. + +## Learn more + +- 🧠 [Nx Docs](/getting-started/intro) +- 👩‍💻 [Tailwind v4 Vite NPM Workspace Demo](https://github.com/juristr/tailwind4-vite-npm-workspaces) +- 📖 [Nx Sync Generators Documentation](/extending-nx/recipes/create-sync-generator) +- 📹 [Nx Youtube Channel](https://www.youtube.com/@nxdevtools) +- 💬 [Nx Official Discord Server](https://go.nx.dev/community) +- 🐦 [Follow me on Twitter/X](https://twitter.com/juristr) diff --git a/docs/blog/images/articles/bg-tailwind-4-guide.avif b/docs/blog/images/articles/bg-tailwind-4-guide.avif new file mode 100644 index 0000000000..509beb6f36 Binary files /dev/null and b/docs/blog/images/articles/bg-tailwind-4-guide.avif differ diff --git a/docs/blog/images/articles/bg-tailwind-4-guide.png b/docs/blog/images/articles/bg-tailwind-4-guide.png new file mode 100644 index 0000000000..d7de690043 Binary files /dev/null and b/docs/blog/images/articles/bg-tailwind-4-guide.png differ diff --git a/docs/blog/images/articles/tailwind-sync-generator.avif b/docs/blog/images/articles/tailwind-sync-generator.avif new file mode 100644 index 0000000000..f3203bfb32 Binary files /dev/null and b/docs/blog/images/articles/tailwind-sync-generator.avif differ