Open-Source Wikis

/

Tailwind CSS

/

Packages

/

@tailwindcss/vite

tailwindlabs/tailwindcss

@tailwindcss/vite

Active contributors: Robin, Philipp, Jordan

Purpose

The Vite plugin. Lets you write import 'tailwindcss' in a CSS file and have Vite handle the rest, including HMR. Implementation: packages/@tailwindcss-vite/src/index.ts (~690 lines, a single file).

Directory layout

packages/@tailwindcss-vite/
├── package.json   # peerDependencies: { vite: ^5 || ^6 || ^7 || ^8 }
├── README.md
├── tsconfig.json
├── tsup.config.ts
└── src/
    └── index.ts   # the entire plugin (~690 lines)

Key abstractions

Symbol Description
tailwindcss(opts): Plugin[] Default export. Returns an array of Vite plugins (pre, post, server, etc.).
Root (internal class) Per-input-file state: cached compiler, scanner, mtimes, dependencies.
createRoot(env, id) Construct a Root for a given file/environment.
createCustomResolver(resolvers) Wraps Vite resolvers so @import and @plugin paths can use Vite aliases.

How it integrates with Vite

The plugin is one factory that returns several Vite plugins because Vite splits responsibilities across the build lifecycle:

graph LR
    PrePlugin[Pre plugin<br/>configResolved/configureServer]
    TransformPlugin[Transform plugin<br/>load + transform]
    PostPlugin[Post plugin<br/>renderChunk]
    HMR[HMR plugin<br/>handleHotUpdate]

    PrePlugin -->|share state| TransformPlugin
    TransformPlugin --> PostPlugin
    TransformPlugin -->|subscribe| HMR

The Root map is keyed by Vite environment (ssr, client, etc.) so SSR and client builds get separate caches. Each Root holds:

  • The compiled tailwindcss.compile(...) result.
  • An @tailwindcss/oxide Scanner.
  • An mtimes map of every file that contributes to the build (CSS imports, JS plugins, configs).
  • The set of accumulated candidates seen so far.

Pseudocode of the per-file lifecycle:

async function transform(src, id) {
  if (!isCss(id)) return
  if (specialQuery(id)) return  // ?worker, ?raw, ?url, ...

  const root = roots.get(env, id) ?? createRoot(env, id)

  if (root.compiler === null) {
    root.compiler = await compile(src, {
      base: dirname(id),
      loadModule, loadStylesheet,
      // resolves through Vite aliases
    })
  }

  // Update mtimes; if any changed, rebuild compiler
  if (anyMtimeChanged(root)) root.compiler = await compile(src, ...)

  root.scanner ??= new Scanner({ sources: root.compiler.sources })

  for (const cand of root.scanner.scan()) root.candidates.add(cand)

  let css = root.compiler.build([...root.candidates])
  return shouldOptimize ? optimize(css, { minify }).code : css
}

The actual implementation is more elaborate because:

  • Vite has both an "old" pre-environment API and a new environment API. The plugin supports both.
  • Vite distinguishes SSR vs client transform calls; the plugin maintains separate Root instances per environment.
  • HMR has to know which CSS files to invalidate when a JS dependency changes — that's what fullRebuildPaths and the mtimes tracking is for.
  • Asset URLs in the input CSS need to be rewritten through Vite's resolver (createCustomResolver).
  • Inline styles (?index=N.css queries) and special queries (?worker, ?raw, ?url, ?commonjs-proxy) need to be skipped or handled differently.

Plugin options

type PluginOptions = {
  optimize?: boolean | { minify?: boolean };
};

Defaults: optimize is enabled when process.env.NODE_ENV === 'production'. Minify follows optimize. Both can be overridden.

File-watcher integration

Unlike the CLI, the plugin doesn't use @parcel/watcher. Vite's existing watcher (chokidar, in dev mode) drives invalidation through handleHotUpdate. The plugin reports fullRebuildPaths to Vite via the module graph so an edit to a JS plugin file invalidates the CSS module that loaded it.

Common pitfalls (and where they're handled)

  • Aliases (@/foo). Resolved via createCustomResolver which wraps Vite's resolver. The fix for @import and @plugin paths landing under aliases shipped in v4.2.4 (PR #19947) and was reinforced by PR #19803 (v4.2.2).
  • Relative paths inside imported CSS. Resolved relative to the importing file, not the entry. Fix referenced in CHANGELOG.md for v4.x under "Fix relative @import and @plugin paths…" (PR #19965).
  • ?index=N.css inline styles. Skipped via INLINE_STYLE_ID_RE constant.
  • Server-only modules scanned by client CSS. Skip the full reload (PR #19745, v4.2.2).

Integration points

  • Reads from tailwindcss (not via @tailwindcss/node) for direct compile.
  • Reads from @tailwindcss/node for env, Instrumentation, optimize, compile, toSourceMap, clearRequireCache.
  • Reads from @tailwindcss/oxide for the Scanner.
  • Hooks into Vite via the Plugin interface only; no private internals are touched (other than reaching into InternalResolveOptions, which the plugin documents as a known caveat).

Entry points for modification

  • The transform entry point is the function returned for transform inside one of the plugin objects in tailwindcss(). Look for the transform: async function(src, id) { ... } block.
  • HMR behavior is in the handleHotUpdate and watcher integration sections.
  • Resolver overrides flow through createCustomResolver and the customCssResolver/customJsResolver variables inside createRoot.

For the equivalents on other bundlers see postcss and webpack.

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

@tailwindcss/vite – Tailwind CSS wiki | Factory