CSS Portal

Unused CSS Finder - Command Line Version

Stylesheets grow. Features get removed, templates change, and the CSS that styled them stays behind. Every extra byte is downloaded by every visitor and has to be read by every developer who touches the file.

The online Unused CSS Finder checks one page at a time. This command-line version is built for whole websites: it crawls your site the way a visitor would, collects every class and id on every page it finds, and tells you which selectors in your stylesheets can never match anything.

Jump to: Quick start · Report viewer · Options · Safelisting · Reading the report · What is checked · What is not checked · Troubleshooting · FAQ

If this site has been useful, we’d love your support! Consider buying us a coffee to keep things going strong!
Download and quick start
  • PHP 7.1 or newerNeeds the curl and dom extensions
  • Command line onlyRuns on your own computer or server
  • Reads, never writesOnly fetches pages; your site is not changed
  • Polite crawlerFollows robots.txt, adjustable speed

Before you begin

  • PHP 7.1 or newer installed for the command line. Check with php -v.
  • The curl, dom and libxml PHP extensions enabled. They are on by default in most installs. The script tells you if one is missing.
  • Permission to scan the site. Only scan sites you own or are allowed to test.
  1. Download and unzip. Save unused_css.php anywhere outside your public web folder, for example a tools folder.
  2. Open a terminal in that folder.

    On Windows, open Command Prompt or PowerShell. If php is not recognised, use the full path to php.exe (for WAMP this is similar to C:\wamp64\bin\php\php8.2.0\php.exe; the version number will differ).

  3. Run it with your site's address.
    macOS / Linux
    php unused_css.php https://example.com/
    Windows
    php unused_css.php https://example.com/
    
    REM or with the full path to PHP
    C:\wamp64\bin\php\php8.2.0\php.exe unused_css.php https://example.com/
  4. Open the report. When it finishes, look in the new unused-css-example.com folder next to where you ran the command, and open report.txt.

Common ways to run it

# Also write cleaned copies of your stylesheets
php unused_css.php https://example.com/ --purge

# Keep any class starting with "btn-" or named "modal", and skip the blog
php unused_css.php https://example.com/ --safelist="btn-*" --safelist=modal --exclude=/blog/

# Go gently on a small server
php unused_css.php https://example.com/ --concurrency=1 --delay=500

# Use a saved settings file
php unused_css.php --config=unused-css.json

The crawl saves its progress as it goes. If it is interrupted, run the same command again and it picks up where it stopped. Add --fresh to start over.

Report viewer

Ran the script? Drop the report.json file it created onto the box below for an easy-to-read, searchable summary. The file is read in your browser and is never uploaded.

Drop report.json here or click to choose the file

You will find it in the unused-css-your-site folder the script created, next to where you ran the command. Just want to see how it looks? .

If this site has been useful, we’d love your support! Consider buying us a coffee to keep things going strong!
About the command-line version

A site-wide tool for cleaner stylesheets

How it works

This script finds that dead weight. It works in three steps:

  1. Crawl. Starting from the address you give it, it follows internal links and reads each page, recording every class, every id and every stylesheet and script the page uses.
  2. Read the CSS. It downloads each stylesheet (including @imported ones and inline <style> blocks) and splits it into individual selectors.
  3. Compare. A selector is reported as unused when it needs a class or id that appears nowhere on the site, in any script, or in your safelist.
The idea in one line

It is a careful estimate, not a proof. It reports what no crawled page uses, so always read what is not checked and review the results before deleting any CSS.

What you get

FileContents
report.txtReadable summary: warnings, then unused classes and ids for each stylesheet, then totals.
report.jsonThe same data for other tools, plus every selector that would be removed and any pages that failed.
purged/*.cssOnly with --purge. Cleaned copies of each stylesheet. Your originals are never touched.

Options

Only the address is required. Everything else has a sensible default. Options can be written --name=value or --name value.

OptionDefaultWhat it does
<url>requiredThe page to start from, usually your home page. https:// is assumed if you leave it off.
--output=DIR./unused-css-<host>Folder for the reports and saved progress.
--purgeoffAlso write cleaned copies of every stylesheet to DIR/purged/.
--max-pages=N5000Stop after N pages. 0 means no limit. If the limit is hit, the report carries a warning.
--concurrency=N4Pages fetched at the same time (1–20).
--delay=MS0Minimum pause between requests, in milliseconds. A Crawl-delay in robots.txt is applied automatically (up to 10 seconds).
--timeout=SEC20How long to wait for a single request (1–120).
--keep-queryoffTreat /page?a=1 and /page?a=2 as different pages. By default query strings are ignored, which is faster and almost always gives the same result.
--exclude=PATTERNnoneDo not crawl URLs that match. Can be repeated.
--safelist=PATTERNnoneAlways keep matching class names. Can be repeated.
--safelist-id=PATTERNnoneAlways keep matching ids. Can be repeated.
--no-default-safelistoffStop automatically keeping active, show, open, hidden, visible, disabled, selected, fade, collapse, collapsing and anything starting with is-, has- or js-.
--no-js-prefixesoffStop treating strings such as 'btn-' in scripts as a reason to keep every class that begins with them.
--external-cssoffAlso analyse stylesheets hosted on other domains, such as a CDN. Usually not what you want.
--external-jsoffAlso read scripts hosted on other domains for class names.
--no-robotsoffIgnore robots.txt. Only use this on a site you own.
--insecureoffSkip SSL certificate checks. Use only for a site you trust, such as a test server with a self-signed certificate.
--user-agent=TEXTUnusedCssFinder/1.0The name the crawler sends to your server.
--freshoffIgnore any saved progress and crawl again from the start.
--config=FILEnoneRead options from a JSON file. Anything typed on the command line wins.
--help, --versionShow the built-in help or the version.

Settings file

For a site you scan regularly, keep the settings in a JSON file. Use the option names without the leading dashes:

{
  "url": "https://example.com/",
  "max-pages": 2000,
  "delay": 250,
  "exclude": ["/blog/", "/forum/*"],
  "safelist": ["btn-*", "modal", "re:^col-(sm|md|lg)-\\d+$"],
  "safelist-id": ["cookie-banner"],
  "purge": true
}

Then run php unused_css.php --config=unused-css.json.

Exit codes

0 means the run completed. 1 means it could not start or finish (bad option, unreachable site, no pages readable). Warnings about an incomplete crawl do not change the exit code, so check the report.

Safelisting

A safelist tells the tool “keep this, even if you cannot see it being used.” You need one when classes appear only at certain moments or are built in ways a crawler cannot read.

Pattern syntax

You writeMeaning
modalExactly modal.
btn-*Anything starting with btn-.
*-iconAnything ending with -icon.
re:^col-(sm|md)-\d+$Anything starting with re: is a regular expression. For a case-insensitive match write re:(?i)^promo.

--safelist and --safelist-id match the whole name. --exclude matches anywhere in the URL, so --exclude=/blog/ skips every address containing /blog/.

On most terminals, put patterns containing *, (, | or $ in quotes so the shell does not change them.

When to safelist

  • Classes added by clicks or timers in another way than plain strings, for example el.className = prefix + name.
  • Classes that only exist in situations the crawler cannot reach: error messages, logged-in screens, seasonal banners, A/B variants.
  • Classes used by third-party widgets that inject their own markup.

Reading the report

The easiest way to read the results is the report viewer above: drop your report.json onto it for a sorted, searchable summary. Everything stays in your browser. The plain-text version, report.txt, looks like this (trimmed):

Unused CSS report for https://example.com/
============================================================
Generated 2026-10-14T00:12:41+00:00 by unused_css.php 1.0.4
Pages read: 214

WARNINGS
  - 3 page(s) could not be read (errors, timeouts or blocked requests).
    Classes used only on those pages may be wrongly listed as unused.

https://example.com/css/site.css?v=14
  selectors: 1,204 | unused: 311 | size: 88,412 -> 61,980 bytes
  unused classes: .promo-old .legacy-nav .tab-3 ...
  unused ids: #old-banner

UNUSED CLASSES (96)
  .legacy-nav
  .promo-old
  ...
  • Warnings come first. If the crawl was cut short or pages failed, treat the list as a set of candidates, not facts.
  • selectors / unused. A selector is one entry in a comma list. .a, .b { } is two selectors; if only .b is unused, .a stays in the cleaned file.
  • size before → after. Applies only to the cleaned copy that --purge writes.
  • report.json lists every individual selector that would be removed, which is useful when checking a surprising result.
About the cleaned files

The files in purged/ are re-written, not minified. Comments are removed and each selector goes on its own line. Rules are kept in their original order. They are meant for review and testing, not as a drop-in replacement for your live CSS.

What is checked

Counted as “used”

  • Every class in a class="…" attribute on any crawled page.
  • Every id="…".
  • Ids referenced by for, aria-controls, aria-labelledby, aria-describedby, aria-owns, list and form attributes.
  • Ids targeted by a link such as <a href="#team"> or /page#team.
  • Every word in inline scripts, in your own .js files, and in data-* attributes. That covers classList.add('is-open'), jQuery selectors and data-bs-target="#menu".
  • Strings ending in - or _ in scripts (for example 'btn-' + type) keep every class that starts with them.
  • Anything on your safelist, plus the default safelist of common state classes.

Where it looks for CSS

  • <link rel="stylesheet"> and <link rel="preload" as="style">
  • Inline <style> blocks
  • @import chains, followed automatically
  • Rules inside @media, @supports, @layer, @container, @document and @scope

Crawling behaviour

  • Follows <a href> links on the same site. example.com and www.example.com count as one site, and a start page that redirects (http→https, or to www) is followed.
  • Respects Disallow rules for User-agent: * and any Crawl-delay.
  • Scans pages marked noindex or with a different canonical address, because their CSS is still in use.
  • Reads non-English class names correctly (UTF-8).

What is not checked

These are the limits of the approach. Some make the tool flag classes that are really in use, and those are the ones to review before you delete anything. Others make it keep classes that are really unused, which is safe but means you may see smaller savings. The rest are things it simply does not analyse.

Things that can make a used class look unused

Review these first

If a class you know is in use appears in the list, one of the reasons below is almost certainly why. Add it to the safelist.

  • JavaScript is not run. The tool reads the HTML the server sends. Content that scripts build in the browser, such as single-page apps (React, Vue, Angular), content loaded with fetch or AJAX, infinite scroll, or markup injected from a template, is invisible to it. For such sites the results are unreliable.
  • Pages it cannot reach. Only links in the HTML are followed. It does not read your sitemap, log in, keep cookies, submit forms or use search. Orphan pages, member areas, checkout steps, search results and pages that exist only behind JavaScript menus are never seen.
  • States that are not showing. Error messages, success banners, flash notices, cookie banners shown only once, seasonal promotions, A/B variants and per-language pages that are not linked are missing unless they happened to be in the HTML it fetched.
  • Class names built in pieces. A prefix such as 'btn-' + type is detected. A suffix (type + '-btn') or a name assembled from several parts in the middle is not.
  • Server-side only differences. If your server sends different HTML to crawlers than to browsers, or blocks the crawler partway, the tool sees only what it was given. Check the warnings for failed pages.
  • Crawl limits. If --max-pages is reached, pages beyond it are not scanned. The report warns you when this happens.
  • Other hosts. Subdomains other than www are treated as different sites and are not crawled. Third-party CSS and JavaScript are skipped unless you opt in.
  • Size limits. Pages over 5 MB and stylesheets or scripts over 20 MB are skipped. Up to five redirects are followed.

Things that can make an unused class look used

  • Words in scripts count. Every word in your JavaScript counts as use, so a class named header is kept if the word “header” appears in any script, even as a variable name. This is deliberate: it avoids deleting classes that scripts really do add.
  • Prefix matching is broad. One string such as 'col-' in a script keeps every class starting with col-. Use --no-js-prefixes if this keeps too much.
  • The default safelist. Classes such as active and is-* are kept even if nothing uses them. Use --no-default-safelist to turn this off.

What the CSS check does not do

  • It does not match selectors against the page structure. A rule like .menu .item is kept if both classes appear anywhere on the site, even if no .item ever sits inside a .menu. Descendant, child and sibling relationships are not tested.
  • Only classes and ids are examined. Tag selectors, attribute selectors ([type=text], [class^="icon-"]), pseudo-classes and pseudo-elements are not checked. A selector with no class or id in it is always kept.
  • Arguments of :not(), :is(), :where() and :has() are ignored. A class used only inside one of those never causes a rule to be flagged.
  • Native CSS nesting is not analysed. The outer rule is checked; rules nested inside its braces are kept untouched.
  • Other unused CSS is left alone. It does not remove unused @keyframes, @font-face, @property, custom properties (--my-color) or declarations inside a rule.
  • It does not check whether a rule has any effect. A rule that is always overridden by another still counts as used if its classes are on a page.
  • Inline style="" attributes are not part of the comparison.

A safe workflow

  1. Run it and read the warnings. If pages failed or the page limit was reached, fix that first (raise --max-pages, lower --concurrency, add --delay).
  2. Scan the list for things you recognise. Anything you know is used goes on the safelist. Pay attention to modals, menus, forms, alerts and anything JavaScript touches.
  3. Run again with your safelist. Repeat until the list holds only classes you are happy to lose.
  4. Write the cleaned files with --purge. Put them on a test copy of the site, not the live one.
  5. Click through the test site. Open menus, trigger errors, try forms and view it logged in and out, on a phone and a desktop. The crawler cannot do these things; you can.
  6. Deploy, and keep the originals. Keep a backup of the old CSS so you can roll back quickly.

Troubleshooting

What you seeWhat to do
'php' is not recognizedPHP is not on your system path. Use the full path to php.exe, or add PHP's folder to the Windows PATH setting.
Parse error: syntax error, unexpected …Your command-line PHP is older than 7.1. Run php -v. On WAMP, the command line can use a different PHP version from the one selected for the web server.
“the PHP … extension is required”Enable curl or dom in the command-line php.ini. Run php --ini to see which file that is, remove the ; in front of the extension line, and try again.
“SSL certificate problem”Your PHP has no certificate list, or the site's certificate is not trusted. Set curl.cainfo in php.ini, or for a site you trust use --insecure.
“The start page returned HTTP 403” (or 429)The site or its firewall is blocking automated requests. Allow your IP address, try --user-agent with a browser-like value, or scan a staging copy. For 429, use --concurrency=1 --delay=1000.
“Only one page was found”The site builds its pages with JavaScript, or its links are not plain <a href> tags. The tool cannot see them. Results will not be reliable.
A class I use is listed as unusedRead what is not checked, then add it to the safelist.
A class I removed long ago is not listedA word with the same spelling appears in a script, or a prefix rule keeps it. Check with --no-js-prefixes.
It takes a long timeLower --max-pages, skip sections with --exclude, or raise --concurrency if your server can take it.
It was interruptedRun the same command again. It resumes. Use --fresh to restart.
“Allowed memory size exhausted”The site is very large. Use --max-pages and --exclude to scan it in parts.

Responsible use

Only scan sites you own or have permission to test. Leave robots.txt checking on unless the site is yours, and pick a speed your server can handle. This script is released under the MIT License, so you are free to use, copy and modify it. It is provided as is, with no warranty. Always keep a backup of your CSS before you change anything.

Frequently Asked Questions

Will it change or break my website?

No. It only requests pages, stylesheets and scripts, the same as a visitor. It never writes to your site, and it never edits your CSS files. The cleaned copies are written next to the report, in a folder you choose.

Can I put it on my server and run it from a web page?

No. It refuses to run in a web request, on purpose. A long crawl would time out, and a public page that fetches any address you give it is a security risk. Run it from a terminal on your computer or over SSH, and keep the file outside your public web folder.

How much load does it put on my server?

By default it makes up to four requests at a time with no pause, about like a few fast visitors. On a small or shared server, use --concurrency=1 --delay=500. The tool also follows any Crawl-delay your robots.txt asks for.

Does it work with WordPress, Laravel, Shopify and so on?

It works with anything that serves normal HTML pages, because it reads the final page rather than your source code. Server-rendered sites work best. Themes that load most of their content with JavaScript will give poor results.

Can it scan a site I run on my own computer?

Yes. Give it the local address, for example http://localhost/mysite/. If the local site uses a self-signed certificate, add --insecure.

Why does it ignore query strings by default?

Addresses like /products?page=2 and /products?page=3 normally share one template, so crawling all of them adds time but no new classes. Use --keep-query if query strings really do change the markup.

Does it send my data anywhere?

No. It only talks to the site you point it at (and, if you enable the external options, to the CDNs that site uses). Reports are written to your own disk.

Why do the cleaned files look different from my originals?

They are re-written by the tool: comments are dropped and each selector sits on its own line. The rules themselves and their order are unchanged. If you minify your CSS in a build step, run that step on the cleaned files.

If this site has been useful, we’d love your support! Consider buying us a coffee to keep things going strong!