Skip to content

Investigating Inconsistent Config Loading with @tailwindcss/vite #18468

@philippedev101

Description

@philippedev101

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 safelist in my tailwind.config.js (like .rotate-12) are correctly generated in the final CSS.
  • Utility classes used in my content files (like .text-blue-500 in index.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:

  1. Incorrect base path calculation in the Vite Plugin.
    In packages/@tailwindcss-vite/src/index.ts, the Root.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 compile function with this incorrect inputBase.
  2. The compat Layer Receives the Wrong Path.
    The core engine's parseCss function (in packages/tailwindcss/src/index.ts) receives this incorrect base path and passes it down to applyCompatibilityHooks in packages/tailwindcss/src/compat/apply-compat-hooks.ts.

  3. Config Resolution Fails to Process User content.
    The applyCompatibilityHooks function is responsible for finding and merging configs.

    • Because our CSS doesn't use an explicit @config directive, the function enters a path to find an implicit tailwind.config.js.
    • However, its call to upgradeToFullPluginSupport and the subsequent calls to resolveConfig (in packages/tailwindcss/src/compat/config/resolve-config.ts) end up processing only the built-in default theme config.
    • Crucially, the user's tailwind.config.js is never added to the list of files for the resolveConfig function to process for its content key.
  4. An Empty content Array is Returned.
    Because the user's config was never processed for its content key in this pipeline, the resolveConfig function returns a final, merged config where content.files is an empty array.

  5. The vite Plugin's Fallback is Triggered.
    The @tailwindcss/vite plugin 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 actual tailwind.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:

  1. Is it the intended behavior for the @tailwindcss/vite plugin to use the CSS file's directory as the base for config resolution, instead of the Vite project root?
  2. The compat layer's resolveConfig doesn't seem to perform an upward search for a config file. Is that the expected logic for that part of the system?
  3. 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/vite being 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions