Next.js
Installation
pnpm add -D tailwindcss-obfuscatornpm install -D tailwindcss-obfuscatoryarn add -D tailwindcss-obfuscatorConfiguration
// next.config.ts
import type { NextConfig } from "next";
import TailwindObfuscator from "tailwindcss-obfuscator/webpack";
const config: NextConfig = {
webpack: (config, { dev }) => {
// Production builds only
if (!dev) {
config.plugins = config.plugins || [];
config.plugins.push(
TailwindObfuscator({
prefix: "tw-",
verbose: true,
})
);
}
return config;
},
};
export default config;Options
TailwindObfuscator({
// Prefix for obfuscated class names
prefix: "tw-",
// Enable verbose logging
verbose: true,
// Exclude specific classes
exclude: ["dark", "light", /^data-/],
// Mapping file output
mapping: {
enabled: true,
file: ".tw-obfuscation/class-mapping.json",
pretty: 2,
},
// Cache for incremental builds
cache: {
enabled: true,
directory: ".tw-obfuscation/cache",
strategy: "merge",
},
});App Router vs Pages Router
The plugin works with both Next.js architectures:
| Router | Support |
|---|---|
| App Router (Next.js 13+) | Stable |
| Pages Router | Stable |
Compatibility
| Next.js | Status |
|---|---|
| 16.x | Stable |
| 15.x | Stable |
| 14.x | Stable |
| 13.x | Stable |
shadcn/ui
For projects using shadcn/ui with cn() and cva(), no extra configuration is required — the plugin detects these helpers automatically.
// Works out of the box
import { cn } from "@/lib/utils";
<div className={cn("flex items-center", isActive && "bg-blue-500")}>;Full example
// next.config.ts
import type { NextConfig } from "next";
import TailwindObfuscator from "tailwindcss-obfuscator/webpack";
const config: NextConfig = {
webpack: (config, { dev, isServer }) => {
// Production client bundle only
if (!dev && !isServer) {
config.plugins = config.plugins || [];
config.plugins.push(
TailwindObfuscator({
prefix: "tw-",
verbose: true,
exclude: ["dark", "light", /^prose/],
mapping: {
enabled: true,
file: ".tw-obfuscation/class-mapping.json",
pretty: false,
},
cache: {
enabled: true,
strategy: "merge",
},
})
);
}
return config;
},
};
export default config;Turbopack
Status — supported via post-build CLI (since v2.0.1)
Turbopack does not expose a plugin API, so tailwindcss-obfuscator/webpack cannot attach to it directly. The supported workaround is the tw-obfuscator CLI run as a post-build step. Pick whichever pattern fits your project.
Pattern A — Post-build CLI (Turbopack-friendly, official)
Let Turbopack do the build, then run the CLI to obfuscate the produced .next/ output. Works for next dev AND next build, no --webpack flag required.
// package.json
{
"scripts": {
"dev": "next dev",
"build": "next build && tw-obfuscator run --build-dir .next --content 'app/**/*.{js,jsx,ts,tsx,mdx}' --content 'components/**/*.{js,jsx,ts,tsx,mdx}' --css 'app/**/*.css'",
},
}What this does, in order:
next buildruns Turbopack normally — no plugin, no obfuscation.tw-obfuscator run:- Extracts every Tailwind class from your sources (the
--contentglobs). - Generates a deterministic mapping (
bg-blue-500 → tw-a, etc.) and writes it to.tw-obfuscation/class-mapping.json. - Transforms every
.css,.html, and.jschunk under--build-dir .next(which covers both.next/static/**and.next/server/**).
- Extracts every Tailwind class from your sources (the
The result is identical to what the Webpack plugin would produce — same mapping, same bundle-size reduction, same source code untouched. The only difference is timing: the obfuscation happens after Turbopack finishes instead of inside it.
A working sample lives in apps/test-nextjs under the build:turbopack script — run pnpm --filter test-nextjs build:turbopack to reproduce.
Pattern B — Opt out of Turbopack with --webpack
If you prefer the Webpack plugin attaching at build time (no post-build step), keep the supported Webpack pipeline:
// package.json
{
"scripts": {
"dev": "next dev --webpack",
"build": "next build --webpack",
},
}# Default in Next 16 — Turbopack runs, no plugin, post-build CLI required
next dev
next build
# Webpack opt-in — our plugin attaches normally, no post-build CLI needed
next dev --webpack
next build --webpackBoth patterns produce the same final output. Pattern A is the right choice if you want to stay on Turbopack's faster dev experience and don't mind a ~1-second extra step at build time. Pattern B is the right choice if you want a single-pass build with no post-processing.
Why isn't Turbopack supported?
Turbopack is not "Webpack rewritten in Rust" — it's a different bundler with a deliberately small public surface. Three things make porting tailwindcss-obfuscator non-trivial:
- No
webpack:callback. Next 16 ignores thewebpack:block innext.config.tswhen Turbopack is the active bundler. Turbopack reads its ownturbopack:block instead, which only acceptsrules,resolveAlias,resolveExtensions, and a small whitelist of loader entries. - No general-purpose plugin API. Our plugin attaches to
compiler.hooks.compilation.tap()andprocessAssets. Turbopack has no equivalent — you cannot register a transform that runs after every CSS chunk is finalised, you cannot add agenerateBundle-style post-pass, and you cannot inspect the module graph. - No third-party loader chain (yet). Turbopack accepts a fixed set of loaders (CSS, PostCSS, MDX, etc.) plus a few webpack-loader-compatible packages whitelisted by Vercel. There is no public way to ship a custom loader to npm and have Turbopack pick it up.
In other words, the same plugin code that runs unmodified across Vite, Webpack, Rollup, and esbuild — thanks to unplugin — has nothing to attach to under Turbopack today.
What would Turbopack support look like?
There are three realistic paths, listed by ambition:
- ✅ Post-build CLI — shipped in v2.0.1. Let Turbopack run, then walk
.next/static/**and.next/server/**and rewrite class strings using the mapping. Exposed astw-obfuscator run --build-dir .next(see Pattern A above). Works today on every Next.js version. unplugin-turbopackadapter — whenunpluginships official Turbopack support, our existing plugin can ride on it. As of April 2026 this is being prototyped but not stable.- Native Turbopack rule — write a Rust extension when Vercel publishes the public plugin SDK. No ETA from Vercel.
Should we invest in 2 and 3 now?
Honest assessment, April 2026:
- Path 1 (post-build CLI) is shipped and unblocks every user immediately — no
--webpackflag, no plugin code, just a CLI run after the build. The output is identical to the Webpack-plugin path. - Path 2 (unplugin adapter) would let us drop the post-build step but doesn't change the output. Low priority while Path 1 works.
- Path 3 (native Rust) is years out — gated entirely on Vercel publishing a public plugin SDK.
If you have specific feedback on the Pattern A flow (slower than expected, missing --content glob coverage, etc.), please open an issue.
How to detect that obfuscation got skipped
Whether you ran Pattern A or B, verify your bundle was actually obfuscated. The fastest sanity check after a build:
# In your own project — should return nothing once obfuscation worked
grep -RE 'class="[^"]*\bbg-blue-500\b' .next/server/app | head -3
# Or check the mapping file exists and is non-empty
cat .tw-obfuscation/class-mapping.json | head -20If you see original Tailwind classes in the output, the obfuscation step didn't run :
- Pattern A: confirm the
tw-obfuscator runcommand finished successfully (check the&&chain in yourbuildscript). Look for[tw-obfuscator] transformed N filesin the build log. - Pattern B: confirm Turbopack didn't sneak back — pure
next build(no--webpackflag) will silently skip the plugin under Next.js 16.