cybrkyd

Find unused CSS with Python

 Fri, 01 Aug 2025 10:49 UTC
Find unused CSS with Python
Image: CC BY 4.0 by cybrkyd

In keeping with the spirit of moving the furniture around, it was clean-up time. My CSS style sheets were looking a little messy. I did not fancy going through these monsters line-by-line, so I wrote a little Python script to help.

I wanted something simple and efficient, which saves me from labouring through my badly-formatted CSS. The script scans HTML and CSS files locally, detecting CSS rules that are not being used and outputs a report. Since I’m using more than one style sheet, the report is organised by CSS file.

In brief: - cssutils is a bit chatty, so the warnings are suppressed. I’m not building or validating my CSS; I’m only looking for unused CSS rules. - The extract_base_selector function extracts base CSS class names, cleaning the selectors by removing pseudo-classes and placeholders. - The find_unused_css function scans all HTML files, collects used class names and then compares them with those in all CSS files. - The report shows classes on the CSS files which are not found anywhere in the HTML files. - You will need to go through your style sheets and comment/remove the obsolete CSS.

Python script to find unused CSS

Python 3.5+ is required since typing.List and typing.Tuple are available from 3.5 onwards.

Install the non-standard library module cssutils with:

pip install cssutils

The script

#!/usr/bin/env python3

import os
import re
import cssutils
import logging
from typing import List, Tuple
from datetime import datetime

# Suppress cssutils warnings
cssutils.log.setLevel(logging.CRITICAL)
cssutils.log.setLevel(logging.FATAL)

def extract_base_selector(selector: str) -> List[str]:
    base_selectors = []
    for sel in selector.split(','):
        clean_sel = re.sub(r':[\w-]+', '', sel)
        clean_sel = re.sub(r'\$.*?\$', '', clean_sel)
        classes = re.findall(r'\.([\w-]+)', clean_sel)
        base_selectors.extend(classes)
    return base_selectors

def find_unused_css(html_dir: str, css_files: List[str]) -> List[Tuple[str, str]]:
    used_classes = set()
    for root, _, files in os.walk(html_dir):
        for file in files:
            if file.endswith('.html'):
                with open(os.path.join(root, file), 'r') as f:
                    content = f.read()
                    found_classes = re.findall(r'class=[\'"]([^\'"]+)', content)
                    for class_list in found_classes:
                        used_classes.update(class_list.split())

    unused_rules = []
    for css_file in css_files:
        try:
            parser = cssutils.CSSParser(raiseExceptions=False, validate=False)
            sheet = parser.parseFile(css_file)
            for rule in sheet:
                if isinstance(rule, cssutils.css.CSSStyleRule):
                    base_selectors = extract_base_selector(rule.selectorText)
                    if base_selectors and not any(sel in used_classes for sel in base_selectors):
                        unused_rules.append((css_file, rule.selectorText))
        except Exception:
            pass  # Suppress any errors during processing

    return unused_rules

def generate_report(unused_rules: List[Tuple[str, str]]):
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    report_filename = f"unused_css_report_{timestamp}.txt"

    with open(report_filename, 'w') as report:
        report.write(f"Unused CSS Report - {timestamp}\n")
        report.write("=" * 40 + "\n\n")
        if not unused_rules:
            report.write("No unused CSS rules found!\n")
        else:
            report.write(f"Total Unused Rules: {len(unused_rules)}\n\n")
            report.write("Detailed Breakdown:\n")
            # Group by CSS file
            css_groups = {}
            for file, selector in unused_rules:
                if file not in css_groups:
                    css_groups[file] = []
                css_groups[file].append(selector)
            for css_file, selectors in css_groups.items():
                report.write(f"\nCSS File: {css_file}\n")
                report.write("-" * 40 + "\n")
                for selector in selectors:
                    report.write(f"  {selector}\n")

    return report_filename

def main():
    html_directory = '/path/to/html/dir'
    css_files = [
        '/path/to/css/styles.css',
        '/path/to/css/bootstrap.css'
    ]
    unused = find_unused_css(html_directory, css_files)
    report_file = generate_report(unused)

if __name__ == "__main__":
    main()

Check for the report with name unused_css_report_{timestamp}.txt in the same folder where the Python script is run from. Here is a sample:

Unused CSS Report - 20250801_105330
========================================

Total Unused Rules: 12

Detailed Breakdown:

CSS File: /path/to/css/styles.css
----------------------------------------
  .header-section.has-img .no-img
  .noted-content
  .noted-item
  .noted-post-time
  .noted-content .noted-item img
  .noted-pagination

CSS File: /path/to/css/bootstrap.css
----------------------------------------
  .sr-only
  .sr-only-focusable:active, .sr-only-focusable:focus
  .page-header
  .pre-scrollable
  .container-fluid
  .table

Use the report to help you clean up the CSS. I recommend commenting out at first before totally removing, and test test test!

Have fun.

»
Tagged in: #python #CSS

Visitors: Loading...