Getting Started

Comprehensive setup guide for implementing Tailwind class obfuscation in your projects. Learn step-by-step installation, script configuration, build process integration, token setup, and best practices. Get your obfuscation workflow running in minutes with detailed examples and troubleshooting tips.

Installation

First, install the necessary packages for your Tailwind setup:

Terminal
npm install tailwindcss postcss autoprefixer
npm install -D @tailwindcss/typography

Obfuscation Script

Create scripts/obfuscate-tailwind.js:

TSX
const fs = require("fs");
const path = require("path");
const { glob } = require("glob");

const CONFIG = {
  filesToScan: [
    "src/**/*.{jsx,tsx,js,ts,html}",
    "pages/**/*.{jsx,tsx}",
    "components/**/*.{jsx,tsx}",
    "app/**/*.{jsx,tsx}",
  ],
  excludePatterns: ["node_modules", ".next", "dist", ".git"],
  outputCssPath: "app/obfuscated-styles.css",
  mapFilePath: ".obfuscation-map.json",
  randomNameLength: 8,
};

const TAILWIND_CLASS_REGEX = /className\s*=\s*["']([^"']+)["']/g;
const CLASS_ATTR_REGEX = /class\s*=\s*["']([^"']+)["']/g;

const CUSTOM_CLASSES = [
  "container-wrapper",
  "container",
  "slider_content",
  "prose",
  "rainbow-banner-gradient-1",
  "rainbow-banner-gradient-2",
  "cpu-architecture",
  "cpu-line-1",
  "cpu-line-2",
  "cpu-line-3",
  "cpu-line-4",
  "cpu-line-5",
  "group",
  "peer",
  "cpu-line-6",
  "cpu-line-7",
  "cpu-line-8",
  "spotlight-main",
  "spotlight-shadow",
  "spotlight-elipse",
  "spotlight-base",
  "spotlight-fade",
  "spotlight-left",
  "spotlight-right",
  "glass-button",
  "glass-btn",
  "gradient-wrapper",
  "mdxcard",
  "animated-btn",
  "blur-vignette",
  "animated-text",
  "scrollbar-none",
  "shiki",
  "dark",
];

const SKIP_CLASSES = ["rounded-rt-lg", "peer", "prose", "not-prose"];

function generateRandomClassName() {
  const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
  const chars = letters + "0123456789";
  let result = letters.charAt(Math.floor(Math.random() * letters.length));
  for (let i = 1; i < CONFIG.randomNameLength; i++) {
    result += chars.charAt(Math.floor(Math.random() * chars.length));
  }
  return result;
}

function isPureCustomClass(classString) {
  const classes = classString.trim().split(/\s+/);
  return classes.every((cls) => CUSTOM_CLASSES.includes(cls));
}

function shouldSkipClass(cls) {
  if (SKIP_CLASSES.includes(cls)) return true;
  if (CUSTOM_CLASSES.includes(cls)) return true;
  if (
    SKIP_CLASSES.some(
      (skip) => cls.startsWith(skip + "-") || cls.startsWith(skip + ":"),
    )
  )
    return true;
  return false;
}

function getValidClassString(classString) {
  if (!classString || classString.trim().length === 0) return null;

  const validClasses = classString
    .trim()
    .split(/\s+/)
    .filter((cls) => {
      if (!cls || cls.length < 2) return false;
      if (cls.includes(",")) return false;
      if (cls.includes("`") || cls.includes("$")) return false;
      if (cls.includes("{") || cls.includes("}")) return false;
      if (cls.endsWith("-")) return false;
      if (cls.endsWith(":")) return false;
      if (shouldSkipClass(cls)) return false;
      if (cls.includes("[") && !cls.includes("]")) return false;
      if (cls.includes("]") && !cls.includes("[")) return false;
      return true;
    });

  if (validClasses.length === 0) return null;
  return validClasses.join(" "); // ← calling .join on a STRING, not array!
}

function extractClassStrings(content) {
  const classStrings = new Set();

  const processRaw = (raw) => {
    if (!raw) return;
    raw = raw.trim();
    if (!raw) return;

    // ✅ Only keep non-skip classes for obfuscation
    const toObfuscate = raw
      .split(/\s+/)
      .filter((cls) => !shouldSkipClass(cls))
      .join(" ");

    const valid = getValidClassString(toObfuscate);
    if (valid) classStrings.add(toObfuscate); // ✅ store only the obfuscatable part
  };

  let match;
  while ((match = TAILWIND_CLASS_REGEX.exec(content)) !== null) {
    processRaw(match[1] || "");
  }
  TAILWIND_CLASS_REGEX.lastIndex = 0;

  while ((match = CLASS_ATTR_REGEX.exec(content)) !== null) {
    processRaw(match[1] || "");
  }
  CLASS_ATTR_REGEX.lastIndex = 0;

  return classStrings;
}

function createClassMapping(classStrings) {
  console.log("🔐 Generating obfuscation mapping...");

  let mapping = {};
  if (fs.existsSync(CONFIG.mapFilePath)) {
    mapping = JSON.parse(fs.readFileSync(CONFIG.mapFilePath, "utf8"));
    console.log(
      `📂 Loaded existing mapping with ${Object.keys(mapping).length} entries`,
    );
  }

  const usedNames = new Set(Object.values(mapping));

  for (const classString of classStrings) {
    if (mapping[classString]) continue;

    let obfuscatedName;
    do {
      obfuscatedName = generateRandomClassName();
    } while (usedNames.has(obfuscatedName));

    usedNames.add(obfuscatedName);
    mapping[classString] = obfuscatedName;
  }

  console.log(`✅ Total mapping: ${Object.keys(mapping).length} class strings`);
  return mapping;
}

function replaceClassesInFile(filePath, mapping) {
  let content = fs.readFileSync(filePath, "utf8");
  let modified = false;

  TAILWIND_CLASS_REGEX.lastIndex = 0;
  CLASS_ATTR_REGEX.lastIndex = 0;

  const makeReplacer =
    () =>
    (match, ...groups) => {
      try {
        const classString = (
          groups.slice(0, 4).find((g) => g != null) || ""
        ).trim();
        if (!classString) return match;

        // ✅ Build the key the same way extractClassStrings does
        const toObfuscate = classString
          .split(/\s+/)
          .filter((cls) => !shouldSkipClass(cls))
          .join(" ");

        const obfuscated = mapping[toObfuscate];
        if (!obfuscated) return match;

        // ✅ Keep skip classes in HTML
        const keepInHtml = classString
          .split(/\s+/)
          .filter((cls) => shouldSkipClass(cls))
          .join(" ");

        const replacement = keepInHtml
          ? `${obfuscated} ${keepInHtml}`
          : obfuscated;

        modified = true;
        return match.split(classString).join(replacement);
      } catch (e) {
        return match;
      }
    };

  try {
    content = content.replace(TAILWIND_CLASS_REGEX, makeReplacer());
  } catch (e) {}
  try {
    content = content.replace(CLASS_ATTR_REGEX, makeReplacer());
  } catch (e) {}

  if (modified) fs.writeFileSync(filePath, content, "utf8");
  return modified;
}

async function replaceAllClasses(mapping) {
  console.log("🔄 Replacing classes in files...");
  let replacedCount = 0;

  for (const pattern of CONFIG.filesToScan) {
    try {
      const files = await glob(pattern, {
        ignore: CONFIG.excludePatterns,
        windowsPathsNoEscape: true,
        dot: true,
      });
      for (const file of files) {
        try {
          if (replaceClassesInFile(file, mapping)) replacedCount++;
        } catch (err) {
          console.warn(`⚠️  Could not process file: ${file}`, err.message);
        }
      }
    } catch (err) {
      console.warn(`⚠️  Error with pattern ${pattern}`, err.message);
    }
  }

  console.log(`✅ Updated ${replacedCount} files`);
}

function generateMappingCss(mapping) {
  let css = "/* Auto-generated obfuscation mapping */\n";
  css += "/* DO NOT EDIT MANUALLY - regenerated on build */\n\n";
  css += '@reference "tailwindcss";\n';
  css += '@reference "./token.css";\n\n';

  for (const [classString, obfuscatedName] of Object.entries(mapping)) {
    // Keep variant classes like dark:, hover:, md:, 2xl: in @apply
    // they work correctly when grouped together in one rule
    const validClasses = classString
      .split(/\s+/)
      .filter((cls) => {
        if (!cls || cls.length < 2) return false;
        if (cls.includes(",")) return false;
        if (cls.includes("`") || cls.includes("$")) return false;
        if (cls.includes("{") || cls.includes("}")) return false;
        if (cls.endsWith("-")) return false;
        if (cls.endsWith(":")) return false;
        if (shouldSkipClass(cls)) return false;
        if (cls.includes("[") && !cls.includes("]")) return false;
        if (cls.includes("]") && !cls.includes("[")) return false;

        return true;
      })
      .join(" ");

    if (!validClasses) continue;

    css += `.${obfuscatedName} { @apply ${validClasses}; }\n`;
  }

  const dir = path.dirname(CONFIG.outputCssPath);
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });

  fs.writeFileSync(CONFIG.outputCssPath, css, "utf8");
  console.log(`✅ Generated CSS file: ${CONFIG.outputCssPath}`);
}

function saveMapping(mapping) {
  fs.writeFileSync(
    CONFIG.mapFilePath,
    JSON.stringify(mapping, null, 2),
    "utf8",
  );
  console.log(`✅ Saved mapping to: ${CONFIG.mapFilePath}`);
}

function addImportToLayout() {
  const layoutPath = "app/layout.tsx";

  if (!fs.existsSync(layoutPath)) {
    console.warn("⚠️  layout.tsx not found, skipping import injection");
    return;
  }

  let content = fs.readFileSync(layoutPath, "utf8");
  const importLine = `import "./obfuscated-styles.css";`;

  if (content.includes(importLine)) {
    console.log("✅ Import already exists in layout.tsx");
    return;
  }

  // Inject BEFORE globals.css so Tailwind utilities always win
  if (content.includes(`import "./globals.css"`)) {
    content = content.replace(
      `import "./globals.css"`,
      `import "./obfuscated-styles.css";\nimport "./globals.css"`,
    );
  } else {
    const lastImportIndex = content.lastIndexOf("import ");
    const endOfLastImport = content.indexOf("\n", lastImportIndex);
    content =
      content.slice(0, endOfLastImport + 1) +
      importLine +
      "\n" +
      content.slice(endOfLastImport + 1);
  }

  fs.writeFileSync(layoutPath, content, "utf8");
  console.log("✅ Added import to layout.tsx");
}

async function main() {
  try {
    console.log("\n🚀 Starting Tailwind class obfuscation...\n");

    console.log("🔍 Scanning for className strings...");
    const allClassStrings = new Set();

    for (const pattern of CONFIG.filesToScan) {
      try {
        const files = await glob(pattern, {
          ignore: CONFIG.excludePatterns,
          windowsPathsNoEscape: true,
          dot: true,
        });
        for (const file of files) {
          try {
            const content = fs.readFileSync(file, "utf8");
            extractClassStrings(content).forEach((s) => allClassStrings.add(s));
          } catch (err) {
            console.warn(`⚠️  Could not read file: ${file}`);
          }
        }
      } catch (err) {
        console.warn(`⚠️  Error with pattern ${pattern}:`, err.message);
      }
    }

    console.log(`✅ Found ${allClassStrings.size} unique className strings`);

    if (allClassStrings.size === 0) {
      console.log("⚠️  No className strings found. Skipping.");
      return;
    }

    const mapping = createClassMapping(allClassStrings);
    await replaceAllClasses(mapping);
    generateMappingCss(mapping);
    saveMapping(mapping);
    addImportToLayout();

    console.log("\n✅ Obfuscation complete!\n");
    console.log(`   - Class strings mapped: ${allClassStrings.size}`);
    console.log(`   - CSS file: ${CONFIG.outputCssPath}`);
    console.log(`   - Mapping file: ${CONFIG.mapFilePath}\n`);
  } catch (err) {
    console.error("❌ Error during obfuscation:", err);
    process.exit(1);
  }
}

main();

Update Build Script

Add the obfuscation step to your package.json scripts:

JSON
{
  "scripts": {
    "dev": "next dev",
    "build": "node scripts/obfuscate-tailwind.js && next build",
    "start": "next start",
    "lint": "eslint"
  }
}

The build process will now:

  1. Run the obfuscation script first
  2. Generate the obfuscated CSS file
  3. Then run the Next.js build

Generated CSS

After the script runs, it will generate app/obfuscated-styles.css. Import this in your app/layout.tsx:

TSX
import "./obfuscated-styles.css";
import "./globals.css";

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}

Create Token File

Create app/token.css for your theme variables:

CSS
/* Theme variables */
:root {
  --background: oklch(1 0 0);
  --foreground: oklch(0.145 0 0);
  --primary: oklch(0.205 0 0);
  --primary-foreground: oklch(0.985 0 0);
  --border: oklch(0.922 0 0);
  --muted: oklch(0.965 0 0);
  --muted-foreground: oklch(0.456 0 0);
}

@theme inline {
  --color-background: var(--background);
  --color-foreground: var(--foreground);
  --color-primary: var(--primary);
  --color-primary-foreground: var(--primary-foreground);
  --color-border: var(--border);
  --color-muted: var(--muted);
  --color-muted-foreground: var(--muted-foreground);
}

Import Token

Add the token import to your app/globals.css:

CSS
@import "tailwindcss";
@import "./token.css";

Output Files

After a successful build, you should see:

  • app/obfuscated-styles.css
  • .obfuscation-map.json

These are generated files and should reflect the current classes found in your project.

The script will:

  • Find all className attributes in your files
  • Generate random obfuscated class names
  • Create a CSS file with @apply rules
  • Update your files with the new class names

Next: Learn how it works with detailed examples.