vite-plugin-tailwind-obfuscator
v1.1.4
Published
Vite plugin that obfuscates Tailwind CSS v4 class names at build time for anti-scraping protection
Maintainers
Readme
vite-plugin-tailwind-obfuscator
Vite plugin that obfuscates Tailwind CSS v4 class names at build time. Replaces readable utility classes (text-lg, bg-gray-950/70, hover:text-gray-900) with short opaque identifiers (_a, _b, _c) in both CSS and JS output.
Use case: anti-scraping. DOM scrapers and AI crawlers can't rely on stable CSS selectors to extract structured data.
Compatibility
- Vite 7+
- Tailwind CSS v4+ (using
@tailwindcss/vite) - Works with any framework that produces
.jsand.cssassets (React, Vue, Solid, etc.)
Install
pnpm add -D vite-plugin-tailwind-obfuscatorPeer dependencies: vite >=7.0.0, postcss >=8.0.0 (postcss is already there with Tailwind).
Usage
// vite.config.ts
import tailwindcss from "@tailwindcss/vite";
import { tailwindObfuscate } from "vite-plugin-tailwind-obfuscator";
export default defineConfig({
plugins: [
tailwindcss(),
tailwindObfuscate({ seed: process.env.OBFUSCATION_SEED }),
],
});Must come after @tailwindcss/vite in the plugin array. Uses enforce: "post" internally and only runs during builds (apply: "build").
How it works
Runs in Vite's generateBundle hook, after Tailwind has generated CSS and Vite has bundled JS. Does not touch source files, parse JSX, or hook into Tailwind's compiler.
- Extract: parse all
.cssassets with postcss, extract every class selector, unescape Tailwind's backslash escapes - Map: build an obfuscation map assigning short names (
_a,_b, ...,_z,_aa,_ab, ...) - Rewrite CSS: replace class selectors while preserving pseudo-classes (
:hover), pseudo-elements (::before), and combinators - Rewrite JS: replace class name strings in JS bundles using whole-token matching within string literals
- Verify: warn if any original class names remain in the output, log replacement stats
What gets obfuscated
Only classes containing at least one of -, :, /, [, or .. Single-word utility classes (flex, grid, hidden, etc.) are skipped because they collide with common JS identifiers.
JS replacement handles
All common patterns in Vite's bundled output:
- Static className:
className:"text-lg font-bold" - Template literals:
`sticky top-0 ${x ? "bg-red" : "bg-blue"}` - Ternaries:
x ? "bg-red-500" : "bg-blue-500" - Object values:
{ default: "border-gray-200", blue: "border-brand-200" } cn()/clsx()/twMerge()calls (their string arguments are already string literals)
Not handled
- Dynamic class construction (
"text-" + size), which is a Tailwind antipattern anyway - CSS Modules (separate system)
Options
interface ObfuscateOptions {
/**
* Seed for deterministic mapping shuffle. Different seeds produce
* different mappings, breaking scrapers' selectors across deploys.
* If omitted, uses alphabetical order (deterministic but predictable).
*/
seed?: string;
/**
* Class names to never obfuscate (e.g., classes referenced by external scripts).
*/
exclude?: string[];
/**
* Emit the class map as `obfuscation-map.json` in the output for debugging.
* @default false
*/
emitMap?: boolean;
/**
* Enable console logging of stats.
* @default true when NODE_ENV !== "production", false otherwise
*/
verbose?: boolean;
/**
* Custom name generator. Receives (originalClass, index) and must return
* a valid CSS identifier.
*/
nameGenerator?: (className: string, index: number) => string;
}Rotating mappings across deploys
Pass a different seed per deploy so scrapers' selectors break each time.
tailwindObfuscate({ seed: process.env.DEPLOY_SHA })Debugging
Enable emitMap to get an obfuscation-map.json in the build output:
tailwindObfuscate({ emitMap: true }){
"text-lg": "_a",
"font-bold": "_b",
"hover:text-gray-900": "_c"
}Excluding classes
If external scripts or browser extensions rely on specific class names:
tailwindObfuscate({ exclude: ["text-lg", "bg-brand-500"] })Custom name generator
Replace the default sequential naming with your own scheme:
tailwindObfuscate({
nameGenerator: (className, index) => `tw${index}`,
})Development
pnpm install
pnpm build # tsup (ESM + CJS + .d.ts)
pnpm test # vitest
pnpm lint # biome
pnpm typecheck # tsc --noEmitLicense
MIT
