
How to Reduce Unused CSS and Shrink Your Stylesheets
Open the Coverage panel on almost any production website and you'll see a long red bar next to the main stylesheet. That red is CSS the browser downloaded, parsed, and held in memory, but never applied to anything on the page. It's common for well over half of a stylesheet to go unused on any given page, especially on sites built with a large UI framework or years of accumulated styles.
Unused CSS matters because stylesheets are render-blocking by default. The browser won't paint anything until it has downloaded and parsed every stylesheet in the <head>. Every extra kilobyte delays the first paint, and on slow mobile connections that delay is very noticeable. This guide shows how to measure the problem, remove dead rules without breaking your site, and structure delivery so each page only gets what it needs.
Step 1: Measure Before You Cut
Don't start deleting anything until you know where the waste is.
Chrome DevTools Coverage
- Open DevTools and press
Cmd + Shift + P(macOS) orCtrl + Shift + P(Windows/Linux). - Type Coverage and choose Show Coverage.
- Click the reload button in the Coverage panel to record from page load.
- Select a CSS file to see used lines marked in one color and unused lines in another in the Sources panel.
Coverage only reflects what happened during that session. A rule for an open dropdown, a validation error, or a dark theme will show as unused unless you triggered it while recording. Interact with the page, open menus, resize the window, and toggle themes before you trust the numbers. Check several page types too, since your blog post template and your checkout page probably use different rules.
Lighthouse
Lighthouse's Reduce unused CSS audit lists stylesheets with significant unused bytes and estimates potential savings. It's a good way to prioritize, but it has the same limitation as Coverage: it only sees one page in one state.
Check What Actually Goes Over the Wire
The file size on disk isn't what users download. Look at the Network panel and compare the transferred size, which reflects compression, with the resource size. A 200 KB stylesheet might transfer as 30 KB with Brotli. Unused CSS still costs parse time and memory after decompression, but the transfer size tells you how much it's slowing the download.
Step 2: Remove Dead Code at the Source
The cleanest wins come from not writing or importing CSS you don't need.
Import Only the Framework Pieces You Use
Many CSS frameworks are modular. If you use Bootstrap's Sass source, import only the parts you need instead of the full bundle:
// Required
@import "bootstrap/scss/functions";
@import "bootstrap/scss/variables";
@import "bootstrap/scss/variables-dark";
@import "bootstrap/scss/maps";
@import "bootstrap/scss/mixins";
@import "bootstrap/scss/utilities";
@import "bootstrap/scss/root";
@import "bootstrap/scss/reboot";
// Only the components this project actually uses
@import "bootstrap/scss/containers";
@import "bootstrap/scss/grid";
@import "bootstrap/scss/buttons";
@import "bootstrap/scss/forms";
@import "bootstrap/scss/nav";
@import "bootstrap/scss/utilities/api";
Dropping unused components like carousels, toasts, and accordions from the import list removes their CSS entirely, and there's no risk of stripping a rule you actually need.
Delete Styles for Features That No Longer Exist
Old stylesheets collect rules for redesigned pages, retired campaigns, and components nobody renders anymore. Search your templates for a class before keeping its CSS:
# Is .promo-banner still used anywhere?
grep -rn "promo-banner" src/ --include="*.html" --include="*.jsx" --include="*.tsx" --include="*.vue"
If nothing turns up, and the class isn't built dynamically, the rule can go. Doing this for a few of the biggest blocks in your stylesheet is often more effective than any tool.
Prefer Scoped Styles
When styles live alongside the component that uses them, through CSS Modules, scoped styles in Vue or Svelte, or similar, deleting the component deletes its CSS. That makes dead code much less likely to pile up in the first place.
Step 3: Automate Removal with PurgeCSS
For large existing stylesheets, especially ones built on a framework, a tool that compares your CSS against your templates saves a lot of time. PurgeCSS is the most widely used option. It scans your content files for anything that looks like a selector and removes rules whose selectors never appear.
npm install --save-dev purgecss
A basic config:
// purgecss.config.cjs
module.exports = {
content: ["./src/**/*.html", "./src/**/*.js", "./src/**/*.jsx"],
css: ["./dist/css/main.css"],
output: "./dist/css/",
safelist: {
standard: ["is-open", "is-active", "has-error"],
deep: [/^modal/],
greedy: [/tooltip/],
},
};
npx purgecss --config ./purgecss.config.cjs
Using It with PostCSS
If you already run PostCSS, use the plugin instead, and only enable it for production builds so development stays fast:
// postcss.config.cjs
const purgecss = require("@fullhuman/postcss-purgecss").default;
module.exports = {
plugins: [
...(process.env.NODE_ENV === "production"
? [
purgecss({
content: ["./src/**/*.html", "./src/**/*.jsx", "./src/**/*.tsx"],
safelist: ["is-open", "is-active", /^data-theme/],
defaultExtractor: (content) =>
content.match(/[\w-/:]+(?<!:)/g) || [],
}),
]
: []),
],
};
The defaultExtractor controls how PurgeCSS pulls candidate class names out of your files. The pattern above keeps characters like / and : that utility frameworks use in class names.
Depending on the version you install, the package may export the plugin as the default export or as the module itself. If require(...).default is undefined, drop the .default.
The Safelist Is the Whole Game
PurgeCSS can only find class names that appear literally in the files it scans. Any class added in a way it can't see gets removed, and your site breaks in production only. Watch for:
- Classes built from strings, such as
"btn-" + variant. Write out full class names or add them to the safelist. - Classes added by third-party scripts, like a slider library adding
.slick-activeor a date picker's markup. - Classes from a CMS or database, where editors can choose classes that never appear in your templates.
- State classes toggled by JavaScript in files you forgot to include in
content.
The safest habit is to write full class names in code and use lookup objects instead of concatenation:
// PurgeCSS can't see these
el.classList.add(`alert-${type}`);
// PurgeCSS can see these
const alertClasses = {
success: "alert-success",
warning: "alert-warning",
danger: "alert-danger",
};
el.classList.add(alertClasses[type]);
You can also mark regions of your CSS to keep, using special comments:
/* purgecss start ignore */
.slick-slide,
.slick-active,
.slick-track {
/* third-party slider state */
}
/* purgecss end ignore */
Utility Frameworks Handle This Differently
Tailwind CSS doesn't need PurgeCSS. It generates only the utilities it finds in your source files, so its output already contains just what you use. The same rule applies though: class names must appear in full in your source code for Tailwind to find them.
Step 4: Minify and Compress
Once dead rules are gone, make what remains as small as possible.
Minification
Minifiers strip whitespace and comments, shorten colors, merge duplicate rules, and drop overridden declarations. Two popular options are cssnano (a PostCSS plugin) and Lightning CSS (a fast Rust-based tool that also handles prefixing and syntax lowering).
npm install --save-dev lightningcss-cli
npx lightningcss --minify --bundle --targets ">= 0.25%" src/main.css -o dist/main.css
Vite already uses a minifier for production CSS, and many frameworks do the same, so check your build output before adding another one.
Compression
Make sure your server or CDN serves CSS with Brotli or gzip. Brotli generally compresses text a bit better than gzip. You can confirm it in the Network panel by looking for content-encoding: br in the response headers. CSS is highly repetitive text, so compression often shrinks it dramatically.
Target Modern Browsers
Autoprefixer and syntax-lowering tools add fallbacks for old browsers. If your browserslist still includes browsers you no longer support, you're shipping prefixes nobody needs. Review your targets:
{
"browserslist": [">0.3%", "last 2 versions", "not dead"]
}
Adjust it to match your real analytics rather than copying a default from an old project.
Step 5: Deliver Less CSS per Page
Even a lean stylesheet can be split so each page loads only what it needs.
Split by Route
Modern bundlers split CSS per entry point or route automatically when you import stylesheets from the components or pages that use them. The checkout page's CSS stays out of the blog's bundle. If your setup produces one giant main.css, look at whether you're importing everything from a single root file.
Use the media Attribute
Stylesheets that only apply in certain conditions don't need to block rendering:
<link rel="stylesheet" href="/css/main.css" />
<link rel="stylesheet" href="/css/print.css" media="print" />
<link rel="stylesheet" href="/css/wide.css" media="(min-width: 64em)" />
The browser still downloads files whose media query doesn't match, but at low priority and without blocking the first render.
Inline Critical CSS, Defer the Rest
For the fastest first paint, inline the small set of styles needed for the visible part of the page and load the rest without blocking:
<head>
<style>
/* Critical: header, hero, base typography */
body {
margin: 0;
font-family: system-ui, sans-serif;
}
.site-header {
display: flex;
align-items: center;
height: 4rem;
}
</style>
<link
rel="preload"
href="/css/main.css"
as="style"
onload="this.onload=null;this.rel='stylesheet'"
/>
<noscript><link rel="stylesheet" href="/css/main.css" /></noscript>
</head>
Keep the inline block small. Inlining too much bloats every HTML response and defeats caching.
Step 6: Write Leaner CSS Going Forward
Tools clean up after the fact. Habits keep the stylesheet small in the first place.
- Use custom properties for variations. One
.buttonrule that readsvar(--button-bg)is smaller than six nearly identical variant rules. - Watch your Sass output. Deeply nested selectors,
@extendchains, and loops can multiply rules. Check the compiled CSS, not just the source. - Rely on modern layout. Grid and Flexbox with
gapreplace a lot of old margin and clearfix code. - Lean on logical properties and shorthand.
margin-inline: autoandinset: 0are shorter than their longhand equivalents. - Add a size budget. Tools like
size-limitor a simple CI check can fail the build when the CSS bundle grows past a threshold, so bloat gets noticed in review rather than months later.
[
{
"path": "dist/assets/*.css",
"limit": "25 KB"
}
]
That snippet is a .size-limit.json config; run it with npx size-limit in CI.
Common Pitfalls
- Trusting a single Coverage run. Rules for hover states, modals, error messages, and other breakpoints will show as unused if you didn't trigger them.
- Purging in development. It slows builds and hides problems until they appear. Run it only for production, and test the production build before deploying.
- Forgetting dynamic content. Blog posts rendered from Markdown, CMS-driven pages, and user-generated content often use classes that don't appear in your templates. Include those sources in the scan or safelist them.
- Chasing zero. Some unused CSS on a given page is fine, especially if the stylesheet is shared and cached across the site. The goal is a small, cacheable file, not a perfect Coverage score.
Conclusion
Reducing unused CSS is a mix of measurement, cleanup, and delivery. Start with Coverage and Lighthouse to find the biggest offenders. Remove dead code at the source by importing only the framework modules you need and deleting styles for retired features. Use PurgeCSS with a careful safelist for large legacy stylesheets, then minify, compress, and split what's left so each page loads only the rules it needs.
Do that, add a size budget to catch regressions, and your stylesheets will stay small enough that the browser can get to the first paint quickly, which is what your users actually notice.


