-
-
Notifications
You must be signed in to change notification settings - Fork 4.8k
Description
Hello Tailwind Team,
First of all, thank you for all the incredible work on v4. The new engine and the dedicated Vite plugin are exciting developments.
I was recently setting up a new project to try out @tailwindcss/vite as an all-in-one replacement for the older PostCSS setup. My mental model, based on past experience, was that the plugin would automatically find my tailwind.config.js at the project root and use it to configure everything. During this process, I ran into some interesting behavior that I wanted to share.
To understand it better, I created a minimal reproduction repository, which can be found here:
➡️ https://github.com/philippedev101/tailwind-vite-bug-repro
To trace the issue, I also cloned the tailwindcss and vite repositories locally and used pnpm overrides to point the MRE to my local builds. I then instrumented several files with extensive logging. (As a side note, I did have to patch the exports map in tailwindcss/package.json to get the local package to import correctly in my project, changing the "import" field from src/index.ts to dist/lib.mjs.)
The minimal repo uses a standard setup, but with one key feature: the main CSS file is in a subdirectory (src/client/main.css). The pnpm overrides can be removed and the issue still persists with the official npm packages.
The Core Observation
When I run the MRE, I see very specific and seemingly contradictory behavior:
- Classes from the
safelistin mytailwind.config.js(like.rotate-12) are correctly generated in the final CSS. - Utility classes used in my
contentfiles (like.text-blue-500inindex.html) are not detected and are missing from the final CSS.
This suggests that the config file is being found and read, but that the content property is being handled differently or ignored by the content scanning pipeline.
The Debugging Trace
My logging within the local builds revealed a very specific chain of events that seems to cause this:
-
Incorrect
basepath calculation in the Vite Plugin.
Inpackages/@tailwindcss-vite/src/index.ts, theRoot.generate()method calculates a base path from the directory of the CSS file being processed, not the Vite project root.- It correctly has
this.base = "/path/to/repro". - But it calculates and uses
inputBase = "/path/to/repro/src/client". - It then calls the core
compilefunction with this incorrectinputBase.
- It correctly has
-
The
compatLayer Receives the Wrong Path.
The core engine'sparseCssfunction (inpackages/tailwindcss/src/index.ts) receives this incorrectbasepath and passes it down toapplyCompatibilityHooksinpackages/tailwindcss/src/compat/apply-compat-hooks.ts. -
Config Resolution Fails to Process User
content.
TheapplyCompatibilityHooksfunction is responsible for finding and merging configs.- Because our CSS doesn't use an explicit
@configdirective, the function enters a path to find an implicittailwind.config.js. - However, its call to
upgradeToFullPluginSupportand the subsequent calls toresolveConfig(inpackages/tailwindcss/src/compat/config/resolve-config.ts) end up processing only the built-in default theme config. - Crucially, the user's
tailwind.config.jsis never added to the list offilesfor theresolveConfigfunction to process for itscontentkey.
- Because our CSS doesn't use an explicit
-
An Empty
contentArray is Returned.
Because the user's config was never processed for itscontentkey in this pipeline, theresolveConfigfunction returns a final, merged config wherecontent.filesis an empty array. -
The
vitePlugin's Fallback is Triggered.
The@tailwindcss/viteplugin receives this resolved config with no content sources. It then activates its own fallback logic, setting the scanner to watch/*in the project root. This is a very broad pattern that doesn't respect the specific, more efficient globs defined in our actualtailwind.config.js.
Summary of Findings:
It appears there are two separate config-loading mechanisms at play. One process seems to correctly find tailwind.config.js and process the safelist, but a second, more complex process, initiated by the @tailwindcss/vite plugin, fails. This second process fails because the plugin passes an incorrect base path to the core engine's compatibility layer, which then doesn't correctly load the content property from the user's config file, causing the JIT scanner to not work as intended.
A Few Questions
This deep dive left me with a few questions about the intended design, which would be super helpful to understand:
- Is it the intended behavior for the
@tailwindcss/viteplugin to use the CSS file's directory as thebasefor config resolution, instead of the Vite project root? - The
compatlayer'sresolveConfigdoesn't seem to perform an upward search for a config file. Is that the expected logic for that part of the system? - Most importantly, what is the recommended way to set up a project like the MRE? It would be incredibly helpful to see a reference example of
@tailwindcss/vitebeing used without a PostCSS pipeline where the CSS entry point is in a subdirectory.
Thank you again for your time and for looking into this. I'm really excited about where the project is headed!
Environment
- OS: Ubuntu 24.04.2 LTS
- Node.js: v22.17.0
- pnpm: 10.12.4
- Rust: rustc 1.88.0 (6b00bc388 2025-06-23)
- Shell: zsh 5.9
- Tailwind CSS Commit:
b24457a9f4101f20a3c3ab8df39debe87564fe8a - Vite Commit:
2e8050e4cd8835673baf07375b7db35128144222