diff --git a/CHANGELOG.md b/CHANGELOG.md
index 69386b4c3..bf6c2e8ea 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+### Added
+
+- Add `@tailwindcss/turbopack` loader for Tailwind CSS v4
+
### Fixed
- Ensure watch mode detects changes to symlinked `@source` files whose real paths aren't otherwise scanned ([#20356](https://github.com/tailwindlabs/tailwindcss/pull/20356))
diff --git a/integrations/utils.ts b/integrations/utils.ts
index fc073d201..9863918d1 100644
--- a/integrations/utils.ts
+++ b/integrations/utils.ts
@@ -612,6 +612,7 @@ function overwriteVersionsInPnpmWorkspace(content: string): string {
workspace.overrides['@tailwindcss/upgrade>tailwindcss'] = resolveVersion(pkg)
workspace.overrides['@tailwindcss/cli>tailwindcss'] = resolveVersion(pkg)
workspace.overrides['@tailwindcss/postcss>tailwindcss'] = resolveVersion(pkg)
+ workspace.overrides['@tailwindcss/turbopack>tailwindcss'] = resolveVersion(pkg)
workspace.overrides['@tailwindcss/vite>tailwindcss'] = resolveVersion(pkg)
workspace.overrides['@tailwindcss/webpack>tailwindcss'] = resolveVersion(pkg)
} else {
diff --git a/packages/@tailwindcss-turbopack/README.md b/packages/@tailwindcss-turbopack/README.md
new file mode 100644
index 000000000..ee54b4d03
--- /dev/null
+++ b/packages/@tailwindcss-turbopack/README.md
@@ -0,0 +1,99 @@
+
+
+
+
+
+
+
+
+
+
+
+ A utility-first CSS framework for rapidly building custom user interfaces.
+
+
+
+
+
+
+
+
+
+---
+
+## Documentation
+
+For full documentation, visit [tailwindcss.com](https://tailwindcss.com).
+
+## Community
+
+For help, discussion about best practices, or feature ideas:
+
+[Discuss Tailwind CSS on GitHub](https://github.com/tailwindlabs/tailwindcss/discussions)
+
+## Contributing
+
+If you're interested in contributing to Tailwind CSS, please read our [contributing docs](https://github.com/tailwindlabs/tailwindcss/blob/main/.github/CONTRIBUTING.md) **before submitting a pull request**.
+
+---
+
+## @tailwindcss/turbopack
+
+A Turbopack loader for Tailwind CSS v4.
+
+## Installation
+
+```sh
+npm install @tailwindcss/turbopack
+```
+
+### Usage
+
+```javascript
+// next.config.js
+module.exports = {
+ turbopack: {
+ rules: {
+ '*.css': {
+ loaders: ['@tailwindcss/turbopack'],
+ as: '*.css',
+ },
+ },
+ },
+}
+```
+
+Then create a CSS file that imports Tailwind:
+
+```css
+/* src/index.css */
+@import 'tailwindcss';
+```
+
+### Options
+
+#### `base`
+
+The base directory to scan for class candidates. Defaults to the current working directory.
+
+```javascript
+{
+ loader: '@tailwindcss/turbopack',
+ options: {
+ base: process.cwd(),
+ },
+}
+```
+
+#### `optimize`
+
+Whether to optimize and minify the output CSS. Defaults to `true` in production mode.
+
+```javascript
+{
+ loader: '@tailwindcss/turbopack',
+ options: {
+ optimize: true, // or { minify: true }
+ },
+}
+```
diff --git a/packages/@tailwindcss-turbopack/package.json b/packages/@tailwindcss-turbopack/package.json
new file mode 100644
index 000000000..abdbbe90e
--- /dev/null
+++ b/packages/@tailwindcss-turbopack/package.json
@@ -0,0 +1,43 @@
+{
+ "name": "@tailwindcss/turbopack",
+ "version": "4.3.3",
+ "description": "A Turbopack loader for Tailwind CSS v4.",
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/tailwindlabs/tailwindcss.git",
+ "directory": "packages/@tailwindcss-turbopack"
+ },
+ "bugs": "https://github.com/tailwindlabs/tailwindcss/issues",
+ "homepage": "https://tailwindcss.com",
+ "scripts": {
+ "build": "tsup-node",
+ "dev": "pnpm run build -- --watch"
+ },
+ "files": [
+ "dist/"
+ ],
+ "publishConfig": {
+ "provenance": true,
+ "access": "public"
+ },
+ "exports": {
+ ".": {
+ "types": "./dist/index.d.ts",
+ "import": "./dist/index.mjs",
+ "require": "./dist/index.js"
+ }
+ },
+ "main": "./dist/index.js",
+ "types": "./dist/index.d.ts",
+ "dependencies": {
+ "@alloc/quick-lru": "^5.2.0",
+ "@tailwindcss/node": "workspace:*",
+ "@tailwindcss/oxide": "workspace:*",
+ "tailwindcss": "workspace:*"
+ },
+ "devDependencies": {
+ "@types/node": "catalog:",
+ "webpack": "catalog:"
+ }
+}
diff --git a/packages/@tailwindcss-turbopack/src/index.cts b/packages/@tailwindcss-turbopack/src/index.cts
new file mode 100644
index 000000000..dbdded748
--- /dev/null
+++ b/packages/@tailwindcss-turbopack/src/index.cts
@@ -0,0 +1,5 @@
+import tailwindLoader from './index.ts'
+
+// CommonJS export for webpack loaders - must be the function directly
+// @ts-ignore
+export = tailwindLoader
diff --git a/packages/@tailwindcss-turbopack/src/index.ts b/packages/@tailwindcss-turbopack/src/index.ts
new file mode 100644
index 000000000..6d02f1f0b
--- /dev/null
+++ b/packages/@tailwindcss-turbopack/src/index.ts
@@ -0,0 +1,287 @@
+import QuickLRU from '@alloc/quick-lru'
+import {
+ compile,
+ env,
+ Features,
+ Instrumentation,
+ normalizePath,
+ optimize,
+ Polyfills,
+} from '@tailwindcss/node'
+import { clearRequireCache } from '@tailwindcss/node/require-cache'
+import { Scanner } from '@tailwindcss/oxide'
+import fs from 'node:fs'
+import path from 'node:path'
+import type { LoaderContext } from 'webpack'
+
+const DEBUG = env.DEBUG
+
+export interface LoaderOptions {
+ /**
+ * The base directory to scan for class candidates.
+ *
+ * Defaults to the current working directory.
+ */
+ base?: string
+
+ /**
+ * Optimize and minify the output CSS.
+ */
+ optimize?: boolean | { minify?: boolean }
+}
+
+interface CacheEntry {
+ mtimes: Map
+ compiler: null | Awaited>
+ scanner: null | Scanner
+ candidates: Set
+ fullRebuildPaths: string[]
+}
+
+const cache = new QuickLRU({ maxSize: 50 })
+
+function getCacheKey(resourceId: string, opts: LoaderOptions): string {
+ return `${resourceId}:${opts.base ?? ''}:${JSON.stringify(opts.optimize)}`
+}
+
+function getContextFromCache(resourceId: string, opts: LoaderOptions): CacheEntry {
+ let key = getCacheKey(resourceId, opts)
+ if (cache.has(key)) return cache.get(key)!
+ let entry: CacheEntry = {
+ mtimes: new Map(),
+ compiler: null,
+ scanner: null,
+ candidates: new Set(),
+ fullRebuildPaths: [],
+ }
+ cache.set(key, entry)
+ return entry
+}
+
+export default async function tailwindLoader(
+ this: LoaderContext,
+ source: string,
+): Promise {
+ let callback = this.async()
+ let options = this.getOptions() ?? {}
+ let inputFile = this.resourcePath
+ let resourceId = this.resource
+ let base = options.base ?? process.cwd()
+ let shouldOptimize = options.optimize ?? process.env.NODE_ENV === 'production'
+ let isCSSModuleFile = inputFile.endsWith('.module.css')
+
+ using I = new Instrumentation()
+
+ DEBUG && I.start(`[@tailwindcss/webpack] ${path.relative(base, inputFile)}`)
+
+ // Bail out early if this is guaranteed to be a non-Tailwind CSS file.
+ {
+ DEBUG && I.start('Quick bail check')
+ let canBail = !/@(import|reference|theme|variant|config|plugin|apply|tailwind)\b/.test(source)
+ if (canBail) {
+ DEBUG && I.end('Quick bail check')
+ DEBUG && I.end(`[@tailwindcss/webpack] ${path.relative(base, inputFile)}`)
+ callback(null, source)
+ return
+ }
+ DEBUG && I.end('Quick bail check')
+ }
+
+ try {
+ let context = getContextFromCache(resourceId, options)
+ let inputBasePath = path.dirname(path.resolve(inputFile))
+
+ // Whether this is the first build or not
+ let isInitialBuild = context.compiler === null
+
+ async function createCompiler() {
+ DEBUG && I.start('Setup compiler')
+ if (context.fullRebuildPaths.length > 0 && !isInitialBuild) {
+ clearRequireCache(context.fullRebuildPaths)
+ }
+
+ context.fullRebuildPaths = []
+
+ DEBUG && I.start('Create compiler')
+ let compiler = await compile(source, {
+ from: inputFile,
+ base: inputBasePath,
+ shouldRewriteUrls: true,
+ onDependency: (depPath) => context.fullRebuildPaths.push(depPath),
+ // In CSS Module files, we have to disable the `@property` polyfill since these will
+ // emit global `*` rules which are considered to be non-pure and will cause builds
+ // to fail.
+ polyfills: isCSSModuleFile ? Polyfills.All ^ Polyfills.AtProperty : Polyfills.All,
+ })
+ DEBUG && I.end('Create compiler')
+
+ DEBUG && I.end('Setup compiler')
+ return compiler
+ }
+
+ // Setup the compiler if it doesn't exist yet
+ context.compiler ??= await createCompiler()
+
+ // Early exit if no Tailwind features are used
+ if (context.compiler.features === Features.None) {
+ DEBUG && I.end(`[@tailwindcss/webpack] ${path.relative(base, inputFile)}`)
+ callback(null, source)
+ return
+ }
+
+ let rebuildStrategy: 'full' | 'incremental' = 'incremental'
+
+ // Track file modification times to CSS files
+ DEBUG && I.start('Register full rebuild paths')
+ {
+ // Report dependencies for config files, plugins, etc.
+ for (let file of context.fullRebuildPaths) {
+ this.addDependency(path.resolve(file))
+ }
+
+ let files = [...context.fullRebuildPaths, inputFile]
+
+ for (let file of files) {
+ let changedTime: number | null = null
+ try {
+ changedTime = fs.statSync(file)?.mtimeMs ?? null
+ } catch {
+ // File might not exist
+ }
+
+ if (changedTime === null) {
+ if (file === inputFile) {
+ rebuildStrategy = 'full'
+ }
+ continue
+ }
+
+ let prevTime = context.mtimes.get(file)
+ if (prevTime === changedTime) continue
+
+ rebuildStrategy = 'full'
+ context.mtimes.set(file, changedTime)
+ }
+ }
+ DEBUG && I.end('Register full rebuild paths')
+
+ if (rebuildStrategy === 'full' && !isInitialBuild) {
+ context.compiler = await createCompiler()
+ }
+
+ let compiler = context.compiler
+
+ // Check if we need to process this file at all
+ if (
+ !(
+ compiler.features &
+ (Features.AtApply | Features.JsPluginCompat | Features.ThemeFunction | Features.Utilities)
+ )
+ ) {
+ DEBUG && I.end(`[@tailwindcss/webpack] ${path.relative(base, inputFile)}`)
+ callback(null, source)
+ return
+ }
+
+ // Setup or update scanner if needed
+ if (context.scanner === null || rebuildStrategy === 'full') {
+ DEBUG && I.start('Setup scanner')
+ let sources = (() => {
+ // Disable auto source detection
+ if (compiler.root === 'none') {
+ return []
+ }
+
+ // No root specified, use the base directory
+ if (compiler.root === null) {
+ return [{ base, pattern: '**/*', negated: false }]
+ }
+
+ // Use the specified root
+ return [{ ...compiler.root, negated: false }]
+ })().concat(compiler.sources)
+
+ context.scanner = new Scanner({ sources })
+ DEBUG && I.end('Setup scanner')
+ }
+
+ // Scan for candidates if utilities are used
+ if (compiler.features & Features.Utilities) {
+ DEBUG && I.start('Scan for candidates')
+ for (let candidate of context.scanner.scan()) {
+ context.candidates.add(candidate)
+ }
+ DEBUG && I.end('Scan for candidates')
+
+ DEBUG && I.start('Register dependency messages')
+ // Add all found files as direct dependencies
+ let resolvedInputFile = path.resolve(base, inputFile)
+ for (let file of context.scanner.files) {
+ let absolutePath = path.resolve(file)
+ // The CSS file cannot be a dependency of itself
+ if (absolutePath === resolvedInputFile) {
+ continue
+ }
+ this.addDependency(absolutePath)
+ }
+
+ // Register context dependencies for glob patterns
+ for (let glob of context.scanner.globs) {
+ // Skip negated patterns
+ if (glob.pattern[0] === '!') continue
+
+ // Avoid adding a dependency on the base directory itself
+ if (glob.pattern === '*' && base === glob.base) {
+ continue
+ }
+
+ this.addContextDependency(path.resolve(glob.base))
+ }
+
+ // Validate that source(...) paths are directories
+ let root = compiler.root
+ if (root !== 'none' && root !== null) {
+ let basePath = normalizePath(path.resolve(root.base, root.pattern))
+ try {
+ let stats = fs.statSync(basePath)
+ if (!stats.isDirectory()) {
+ throw new Error(
+ `The path given to \`source(…)\` must be a directory but got \`source(${basePath})\` instead.`,
+ )
+ }
+ } catch (err) {
+ if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {
+ throw err
+ }
+ // Directory doesn't exist yet, which is fine
+ }
+ }
+ DEBUG && I.end('Register dependency messages')
+ }
+
+ DEBUG && I.start('Build utilities')
+ let css = compiler.build([...context.candidates])
+ DEBUG && I.end('Build utilities')
+
+ // Optionally optimize the output
+ let result = css
+ if (shouldOptimize) {
+ DEBUG && I.start('Optimization')
+ let optimized = optimize(css, {
+ minify: typeof shouldOptimize === 'object' ? shouldOptimize.minify : true,
+ })
+ result = optimized.code
+ DEBUG && I.end('Optimization')
+ }
+
+ DEBUG && I.end(`[@tailwindcss/webpack] ${path.relative(base, inputFile)}`)
+ callback(null, result)
+ } catch (error) {
+ // Clear the cache entry on error to force a full rebuild next time
+ let key = getCacheKey(resourceId, options)
+ cache.delete(key)
+
+ DEBUG && I.end(`[@tailwindcss/webpack] ${path.relative(base, inputFile)}`)
+ callback(error as Error)
+ }
+}
diff --git a/packages/@tailwindcss-turbopack/tsconfig.json b/packages/@tailwindcss-turbopack/tsconfig.json
new file mode 100644
index 000000000..d2131cbeb
--- /dev/null
+++ b/packages/@tailwindcss-turbopack/tsconfig.json
@@ -0,0 +1,6 @@
+{
+ "extends": "../tsconfig.base.json",
+ "compilerOptions": {
+ "verbatimModuleSyntax": false,
+ },
+}
diff --git a/packages/@tailwindcss-turbopack/tsup.config.ts b/packages/@tailwindcss-turbopack/tsup.config.ts
new file mode 100644
index 000000000..998eebe4f
--- /dev/null
+++ b/packages/@tailwindcss-turbopack/tsup.config.ts
@@ -0,0 +1,19 @@
+import { defineConfig } from 'tsup'
+
+export default defineConfig([
+ {
+ format: ['esm'],
+ clean: true,
+ minify: true,
+ cjsInterop: true,
+ dts: true,
+ entry: ['src/index.ts'],
+ },
+ {
+ format: ['cjs'],
+ minify: true,
+ cjsInterop: true,
+ dts: true,
+ entry: ['src/index.cts'],
+ },
+])
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index b5b2d1c14..8679c69bc 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -431,6 +431,28 @@ importers:
specifier: ^7.7.1
version: 7.7.1
+ packages/@tailwindcss-turbopack:
+ dependencies:
+ '@alloc/quick-lru':
+ specifier: ^5.2.0
+ version: 5.2.0
+ '@tailwindcss/node':
+ specifier: workspace:*
+ version: link:../@tailwindcss-node
+ '@tailwindcss/oxide':
+ specifier: workspace:*
+ version: link:../../crates/node
+ tailwindcss:
+ specifier: workspace:*
+ version: link:../tailwindcss
+ devDependencies:
+ '@types/node':
+ specifier: 'catalog:'
+ version: 22.19.21
+ webpack:
+ specifier: 'catalog:'
+ version: 5.108.3(esbuild@0.27.7)(postcss@8.5.16)
+
packages/@tailwindcss-vite:
dependencies:
'@tailwindcss/node':