Search code examples
tailwind-csssveltesveltekittailwind-css-4

How to remove unused colors during build?


When I run the build script in my svelte kit tailwind project, with Tailwind v4, it add every single tailwind color. I only use one single tailwind color, so I'd like to remove them.

I spent a while searching online and asking LLMs, and nothing has an answer for tailwind v4 which is config-less.

Here's my app.css

@import 'tailwindcss';

@theme {
    --color-brand-red: #f00;
}

@utility container {
    padding-inline: 1rem;
    margin-inline: auto;
}

Here's my package.json:

"scripts": {
        "dev": "vite dev",
        "build": "vite build",
        "preview": "vite preview",
        "prepare": "svelte-kit sync || echo ''",
        "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
        "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
        "format": "prettier --write .",
        "lint": "prettier --check . && eslint ."
    },
    "devDependencies": {
        "@eslint/compat": "^1.2.5",
        "@eslint/js": "^9.18.0",
        "@sveltejs/adapter-auto": "^4.0.0",
        "@sveltejs/kit": "^2.16.0",
        "@sveltejs/vite-plugin-svelte": "^5.0.0",
        "@tailwindcss/vite": "^4.0.0",
        "eslint": "^9.18.0",
        "eslint-config-prettier": "^10.0.1",
        "eslint-plugin-svelte": "^2.46.1",
        "globals": "^15.14.0",
        "prettier": "^3.4.2",
        "prettier-plugin-svelte": "^3.3.3",
        "prettier-plugin-tailwindcss": "^0.6.11",
        "svelte": "^5.0.0",
        "svelte-check": "^4.0.0",
        "tailwindcss": "^4.0.0",
        "typescript": "^5.0.0",
        "typescript-eslint": "^8.20.0",
        "vite": "^6.0.0"
    },
    "pnpm": {
        "onlyBuiltDependencies": [
            "esbuild"
        ]
    },
    "dependencies": {
        "lucide-svelte": "^0.475.0"
    }

Yet when I run pnpm build I get a css file with:

/*! tailwindcss v4.0.6 | MIT License | https://tailwindcss.com */@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-serif:ui-serif,Georgia,Cambria,"Times New Roman",Times,serif;--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-red-50:oklch(.971 .013 17.38);--color-red-100:oklch(.936 .032 17.717);--color-red-200:oklch(.885 .062 18.334);--color-red-300:oklch(.808 .114 19.571);--color-red-400:oklch(.704 .191 22.216);--color-red-500:oklch(.637 .237 25.331);--color-red-600:oklch(.577 .245 27.325);--color-red-700:oklch(.505 .213 27.518);--color-red-800:oklch(.444 .177 26.899);--color-red-900:oklch(.396 .141 25.723);--color-red-950:oklch(.258 .092 26.042);--color-orange-50:oklch(.98 .016 73.684);--color-orange-100:oklch(.954 .038 75.164);--color-orange-200:oklch(.901 .076 70.697);--color-orange-300:oklch(.837 .128 66.29);--color-orange-400:oklch(.75 .183 55.934);--color-orange-500:oklch(.705 .213 47.604);--color-orange-600:oklch(.646 .222 41.116);--color-orange-700:oklch(.553 .195 38.402);--color-orange-800:oklch(.47 .157 37.304);--color-orange-900:oklch(.408 .123 38.172);--color-orange-950:oklch(.266 .079 36.259);--color-amber-50:oklch(.987 .022 95.277);--color-amber-100:oklch(.962 .059 95.617);--color-amber-200:oklch(.924 .12 95.746);--color-amber-300:oklch(.879 .169 91.605);--color-amber-400:oklch(.828 .189 84.429);--color-amber-500:oklch(.769 .188 70.08);--color-amber-600:oklch(.666 .179 58.318);--color-amber-700:oklch(.555 .163 48.998);--color-amber-800:oklch(.473 .137 46.201);--color-amber-900:oklch(.414 .112 45.904);--color-amber-950:oklch(.279 .077 45.635);--color-yellow-50:oklch(.987 .026 102.212);--color-yellow-100:oklch(.973 .071 103.193);--color-yellow-200:oklch(.945 .129 101.54);

And so on. This file is 28kb and I have very few unique tailwind classes I'm using.

I searched their official docs and they only have docs on this for v2 it seems. Is there a way to edit my app.css to remove all unused colors? Thanks.


Solution

  • My previous tests without Vite

    The question is only interesting to me because TailwindCSS doesn't even include unused variables and classes in the first place. So if the goal is to remove "unused" variables, I believe that was solved long ago. There was a bug in TailwindCSS v4 that left them in, but it was quickly fixed, probably in v4.0.5.

    It definitely works well in the CLI and with PostCSS. I looked into this in more depth during a native conversion. See:

    --color-*: initial, why it's a half-finished solution

    I was surprised to find that the issue hasn't been solved in Vite yet. That's probably why you brought up the question. I think the answer you provided doesn't actually solve the problem; it just hides it. Your solution effectively disables the default classes, but if you still add 20 colors and only end up using 3, the build will still include all 20 colors.

    Workaround solution to ensure compression: PurgeCSS

    I would find it worth reporting the bug. In the meantime, use the PurgeCSS plugin to ensure that all unused variables and classes are removed from the output.

    Dependencies
    npm install tailwindcss @tailwindcss/vite postcss @fullhuman/postcss-purgecss
    
    Configuration

    vite.config.js

    import { defineConfig } from 'vite'
    import vue from '@vitejs/plugin-vue'
    import tailwindcss from '@tailwindcss/vite'
    import { purgeCSSPlugin } from '@fullhuman/postcss-purgecss'
    
    // Custom PostCSS plugin to remove comments
    const removeCommentRules = (root: any) => {
      root.walkComments((comment: any) => {
        comment.remove()
      })
    }
    
    // https://vite.dev/config/
    export default defineConfig({
      plugins: [
        vue(), // I know you're using Svelte, so naturally, replace the relevant configurations, but the important part is really just PurgeCSS.
        tailwindcss(),
      ],
      css: {
        postcss: {
          plugins: [
            purgeCSSPlugin({
              content: [
                './**/*.html',
                './**/*.js',
                './**/*.jsx',
                './**/*.ts',
                './**/*.tsx',
              ],
              defaultExtractor: content => content.match(/[\w-/:.\[\]\(\)_\[\]]+(?<!:)/g) || [],
              variables: true,  // Remove unused CSS variables
              keyframes: true,  // Remove unused animations
              fontFace: true,   // Remove unused font faces
            }),
            removeCommentRules,
          ]
        }
      }
    })
    

    style.css

    @import 'tailwindcss';
    
    /* ... */
    
    Test
    npm run build
    

    The result went from 18KB to 4KB. This can be further optimized if you don't need other parts in the output, as mentioned in the linked TailwindCSS to nativeCSS post.

    Tested steps

    • Disabling automatic source detection with source(none) -> Not a good result (18kb with unused variables)
    • Excluding files and directories from automatic source detection with .gitignore -> Not a good result (18kb with unused variables)
    • Using @tailwindcss/postcss plugin instead of the more compatible vite plugin -> Not a good result (18kb with unused variables)
    • Using @tailwindcss/postcss plugin and purgecss -> not recommended for Vue projects, would work without Vite (Specific error due to Vue; It probably would have worked in a native JavaScript Vite project; But it's no coincidence that we have a Vite-specific plugin from TailwindCSS.)
    • Using @tailwindcss/vite plugin and purgecss -> successful (4kb without unused variables)

    v4-related content

    Why can't exclude files from TailwindCSS when using Vite?

    When using @tailwindcss/vite, Tailwind uses the Vite module graph to scan code for class names, rather than the file system huelistics (when not using @tailwindcss/vite). Thus these other class names might be appearing in some compiled code.

    You may be able to switch to the file-based scanning in Vite once tailwindlabs/tailwindcss PR #16425 is in.

    Thanks for @Wongjn!