Integrations
Verified fixtures live under app/ in the repo.
| Fixture | Entry | Command |
|---|---|---|
app/next-app | Next 15 · App Router · Tailwind 4 | pnpm dev · port 3016 |
app/vite-app | Vite · React 18 · Tailwind 3 | pnpm dev:vite · 3017 |
app/next12-app | Next 12 · Pages Router · Tailwind 3 | pnpm dev:next12 · 3018 |
app/next-turbo-app | Next 15 · Turbopack | pnpm dev:turbo · 3019 |
app/vite-v4-app | Vite · Tailwind 4 | pnpm dev:vite4 · 3020 |
app/astro-app | Astro 5 · Tailwind 4 | pnpm dev:astro · 3021 |
Next.js 15 (App Router, Tailwind 4)
// next.config.ts
import {withTailwindcssAtomic} from "tailwindcss-atomic/next";
const nextConfig = {reactStrictMode: true};
export default withTailwindcssAtomic(nextConfig);Keep PostCSS with @tailwindcss/postcss then tailwindcss-atomic/postcss.
Wrappers that return a Next config function (withPostHogConfig, Sentry, some next-intl setups) must be invoked, not spread. withTailwindcssAtomic now unwraps those functions so your webpack() fallbacks stay intact. PostHog still wants to be the outermost wrapper:
export default withPostHogConfig(
withTailwindcssAtomic(withNextIntl(nextConfig), {report: true}),
{ /* posthog */ },
);Next.js 15 + Tailwind 3 + SCSS
Next.js 15 + Tailwind 3 + SCSS
Tailwind v3 does not expand @use 'tailwindcss/utilities'; it expects @tailwind utilities. If the entry is SCSS (for example scss/styles.scss imported from app/layout.tsx) warmup normalizes those layers and, if sass is installed, compiles local @use before PostCSS. That way the flex → _xxxxxx map exists before Webpack transforms App Router bundles (client, SSR, and RSC).
// scss/styles.scss
@use 'tailwindcss/base';
@use 'tailwindcss/components';
@use 'tailwindcss/utilities';
@use './themes/brand' as themes;// app/layout.tsx
import "../scss/styles.scss";// postcss.config.js — atomic plugin last
module.exports = {
plugins: {
"postcss-import": {},
"tailwindcss/nesting": {},
tailwindcss: {},
autoprefixer: {},
"tailwindcss-atomic/postcss": {},
},
};// next.config.ts
import {withTailwindcssAtomic} from "tailwindcss-atomic/next";
export default withTailwindcssAtomic({reactStrictMode: true});withTailwindcssAtomic injects the Webpack loader (pre) and the plugin that, in processAssets, atomicizes all CSS and then rewrites all JS (including server chunks). In next dev it invalidates JS modules when the map changes.
The classic path app/globals.css with @tailwind base/components/utilities still works the same.
Next.js 15.5+ / 16 + Turbopack
// next.config.ts
import {withTailwindcssAtomic} from "tailwindcss-atomic/next";
export default withTailwindcssAtomic({
reactStrictMode: true,
});{ "scripts": { "dev": "next dev --turbopack --port 3019" } }withTailwindcssAtomic fills turbopack.rules with the TS/JS loader (plus the Webpack hook). In Next 16.3+ rules are an array with condition: "foreign" and condition: { not: "foreign" }; the { foreign, default } shorthand no longer validates. Tailwind 4 PostCSS is still required.
Turborepo / monorepo (Next + Turbopack)
A mixed className (flex … hover:bg-revamp-… _cafc46 _ffc2a9) means CSS was atomicized but part of the JS was not rewritten. In Webpack the plugin walks every chunk in processAssets. Turbopack has no such step: it only rewrites modules that go through the loader.
That happens when cva() / cn() lives in a design system outside the app (packages/ui, node_modules/ui-latamwin) and Turbopack rules only touched apps/webs/latamwin.
withTailwindcssAtomic now:
- Puts the loader in
turbopack.rulesfor*.ts(x),*.js(x),*.mjs, and*.cjsas an array of two rules:condition: "foreign"(node_modules / workspace) andcondition: { not: "foreign" }(app code). The loader is a no-op onreact/next; it does rewritetranspilePackagesand workspace junctions. - Sets
turbopack.rootandoutputFileTracingRootto the monorepo root (turbo.json/pnpm-workspace.yaml) if you did not set them. - Adds workspace packages the app declares in
dependenciestotranspilePackages. - Warms up every CSS entry it finds (not just the first). You can force the web’s entry:
// apps/webs/latamwin/next.config.ts
import {withTailwindcssAtomic} from "tailwindcss-atomic/next";
const nextConfig = {
reactStrictMode: true,
transpilePackages: ["ui-latamwin"],
};
export default withTailwindcssAtomic(nextConfig, {
cssEntries: ["scss/styles.scss"],
});If a UI package is published as dist/*.js and is not in transpilePackages, its flex / bg-* strings reach the DOM untouched and mix with the app hashes. Add it to transpilePackages or import source (exports → src).
Do not hash that UI dist in Rollup (preserveModules should keep original utilities). Do not pass { library: true } to withTailwindcssAtomic in the Next app — that option is only for the design-system Rollup build, and the Next adapter ignores it.
If bg-opacity-50 is in the DOM but there is no background-color, twMerge v3 dropped bg-neutral-300. Use bg-neutral-300/50. See Preserve & runtime.
Next.js 12 (Pages Router, Tailwind 3)
// next.config.mjs
import {withTailwindcssAtomic} from "tailwindcss-atomic/next";
export default withTailwindcssAtomic({
reactStrictMode: true,
});Use pages/, styles/globals.css with @tailwind base/components/utilities, and the v3 PostCSS setup from Getting started.
Vite (Tailwind 4)
No tailwind.config.js is required. The CSS entry uses @import "tailwindcss" (and optionally @theme).
The recommended path with Vite is @tailwindcss/vite. You do not need @tailwindcss/postcss or postcss.config:
// vite.config.ts
import {defineConfig} from "vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";
import tailwindcssAtomic from "tailwindcss-atomic/vite";
export default defineConfig({
plugins: [react(), tailwindcss(), tailwindcssAtomic()],
});If you prefer PostCSS instead of the Vite plugin:
// postcss.config.mjs
export default {
plugins: {
"@tailwindcss/postcss": {},
"tailwindcss-atomic/postcss": {},
},
};// vite.config.ts
import {defineConfig} from "vite";
import react from "@vitejs/plugin-react";
import tailwindcssAtomic from "tailwindcss-atomic/vite";
export default defineConfig({
plugins: [react(), tailwindcssAtomic()],
});Vite (Tailwind 3)
import {defineConfig} from "vite";
import react from "@vitejs/plugin-react";
import tailwindcssAtomic from "tailwindcss-atomic/vite";
export default defineConfig({
plugins: [react(), tailwindcssAtomic()],
});v3 PostCSS goes after tailwindcss and autoprefixer.
Astro (Tailwind 4)
Astro uses Vite. Tailwind v4 goes with @tailwindcss/vite (not @astrojs/tailwind). The tailwindcss-atomic/astro integration injects class rewrite in .astro, extracted JS, and CSS:
// astro.config.ts
import {defineConfig} from "astro/config";
import tailwindcss from "@tailwindcss/vite";
import tailwindcssAtomic from "tailwindcss-atomic/astro";
export default defineConfig({
integrations: [tailwindcssAtomic()],
vite: {
plugins: [tailwindcss()],
},
});---
import "../styles/global.css";
---
<div class="flex p-6">Hello</div>If you prefer not to use the integration, the Vite plugin does the same:
import tailwindcssAtomic from "tailwindcss-atomic/vite";
export default defineConfig({
vite: {
plugins: [tailwindcss(), tailwindcssAtomic()],
},
});Webpack
const tailwindcssAtomic = require("tailwindcss-atomic/webpack");
module.exports = {
plugins: [tailwindcssAtomic()],
};Rollup
import tailwindcssAtomic from "tailwindcss-atomic/rollup";
export default {
plugins: [tailwindcssAtomic()],
};esbuild
import {build} from "esbuild";
import tailwindcssAtomic from "tailwindcss-atomic/esbuild";
await build({
entryPoints: ["src/main.ts"],
bundle: true,
outfile: "dist/index.js",
plugins: [tailwindcssAtomic()],
});