Build a CSS Unused Rules Finder Extension: Complete Developer Guide
In the world of web development, CSS files tend to grow organically over time. As projects evolve, developers add new styles, refactor components, and sometimes leave behind orphaned CSS rules that no longer apply to any element on the page. These unused CSS rules, often called dead CSS or unused styles, contribute to larger file sizes, slower page loads, and decreased performance. Building a CSS unused rules finder extension is one of the most valuable tools a developer can create to combat this problem.
This comprehensive guide walks you through the process of building a production-ready Chrome extension that identifies unused CSS rules, helping developers clean up their stylesheets and optimize their websites for better performance.
Why You Need a CSS Unused Rules Finder Extension
Before diving into the code, let’s explore why detecting unused CSS matters and how a dedicated Chrome extension can help solve this pervasive problem.
The Problem with Unused CSS
Modern web applications often include massive stylesheets that contain thousands of lines of CSS. Studies have shown that many production websites ship significantly more CSS than they actually use. A typical website might load 500KB of CSS but only render styles that use a fraction of those rules. This wasted CSS affects performance in several critical ways.
First, browsers must parse and process all loaded CSS, even rules that never match any element. This parsing time adds to the total page load duration. Second, larger CSS files mean more data needs to be transferred over the network, increasing latency and bandwidth costs. Third, the browser’s style calculation process becomes more expensive when it has to evaluate more rules, even unused ones.
Existing Solutions and Their Limitations
Chrome DevTools already includes a CSS coverage feature that can identify unused CSS. You can access it by opening DevTools (F12), going to the three-dot menu, selecting “More tools,” and then choosing “Coverage.” While this built-in tool is useful for manual inspections, it has significant limitations for developers who want automated, repeatable analysis.
The DevTools coverage tool requires manual activation each time you want to analyze a page. It doesn’t provide an easy way to export results, integrate into CI/CD pipelines, or run across multiple pages automatically. Additionally, it shows coverage data in a raw format that requires interpretation.
A custom dead CSS finder chrome extension can address these limitations by providing a streamlined interface, persistent results storage, batch analysis capabilities, and integration with development workflows. Building this extension teaches valuable concepts about Chrome extension development while creating a genuinely useful tool for your development toolkit.
Architecture Overview
Our CSS Unused Rules Finder Extension will leverage Chrome’s powerful APIs to analyze web pages and identify CSS rules that don’t match any elements in the DOM.
Core Components
The extension consists of three main components that work together to provide comprehensive unused CSS detection.
The background script handles extension lifecycle events, manages the popup interface, and coordinates communication between different parts of the extension. It serves as the central hub that ties everything together.
The content script runs in the context of web pages and performs the actual CSS analysis. It uses the CSS Object Model to examine all loaded stylesheets and compares their rules against the current DOM to identify matches.
The popup interface provides users with controls to initiate scans, view results, and export data. It offers an intuitive way to interact with the extension without leaving the browser.
Project Setup
Let’s start building the extension by setting up the project structure and manifest configuration.
Creating the Manifest
Every Chrome extension begins with a manifest file that defines its capabilities and permissions. Create a file named manifest.json in your project directory with the following configuration:
{
"manifest_version": 3,
"name": "CSS Unused Rules Finder",
"version": "1.0.0",
"description": "Identify and remove unused CSS rules from any webpage",
"permissions": [
"activeTab",
"scripting",
"storage"
],
"host_permissions": [
"<all_urls>"
],
"action": {
"default_popup": "popup.html",
"default_icon": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
}
},
"background": {
"service_worker": "background.js"
}
}
This manifest requests the necessary permissions for analyzing web pages. The activeTab permission allows us to execute scripts in the currently active tab, while scripting enables the CSSOM analysis capabilities.
Directory Structure
Create the following directory structure for your extension:
css-unused-finder/
├── manifest.json
├── background.js
├── content.js
├── popup.html
├── popup.css
├── popup.js
├── icons/
│ ├── icon16.png
│ ├── icon48.png
│ └── icon128.png
└── styles/
└── results.css
Implementing the Content Script
The content script is the heart of our extension. It performs the actual work of analyzing CSS rules and identifying which ones are unused.
CSS Analysis Logic
Create content.js with the following implementation:
// Content script for analyzing CSS usage
(function() {
'use strict';
// Store analysis results
let analysisResults = {
totalRules: 0,
unusedRules: 0,
unusedPercentage: 0,
details: []
};
// Analyze all stylesheets in the document
function analyzeStylesheets() {
const styleSheets = document.styleSheets;
const results = [];
for (let i = 0; i < styleSheets.length; i++) {
try {
const sheet = styleSheets[i];
const rules = sheet.cssRules || sheet.rules;
if (!rules) continue;
for (let j = 0; j < rules.length; j++) {
const rule = rules[j];
// Skip at-rules (like @media, @font-face)
if (rule.type !== CSSRule.STYLE_RULE) {
continue;
}
const selectorText = rule.selectorText;
const isUsed = checkSelectorUsage(selectorText);
results.push({
selector: selectorText,
cssText: rule.cssText,
isUsed: isUsed,
sheet: sheet.href || 'inline'
});
}
} catch (e) {
// Handle cross-origin stylesheets
console.warn('Cannot access stylesheet:', e);
}
}
return results;
}
// Check if a selector matches any element in the DOM
function checkSelectorUsage(selector) {
try {
// Handle multiple selectors separated by commas
const selectors = selector.split(',').map(s => s.trim());
for (const sel of selectors) {
if (sel && document.querySelector(sel)) {
return true;
}
}
return false;
} catch (e) {
// Invalid selector, consider it unused
return false;
}
}
// Calculate statistics from analysis results
function calculateStats(results) {
const totalRules = results.length;
const unusedRules = results.filter(r => !r.isUsed).length;
const unusedPercentage = totalRules > 0
? Math.round((unusedRules / totalRules) * 100)
: 0;
return {
totalRules,
unusedRules,
unusedPercentage,
details: results
};
}
// Message handler for communication with popup
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === 'analyze') {
const results = analyzeStylesheets();
const stats = calculateStats(results);
sendResponse(stats);
}
return true;
});
})();
This content script provides the core functionality for detecting unused CSS. It iterates through all stylesheets in the page, examines each CSS rule, and uses document.querySelector() to determine whether the selector matches any element in the DOM.
The script handles several important edge cases. It skips at-rules like @media and @font-face since these aren’t style rules that apply to elements. It gracefully handles cross-origin stylesheets that might throw security errors. It also properly handles selectors with multiple parts separated by commas.
Creating the Background Service Worker
The background script manages extension state and handles communication between the popup and content scripts. Create background.js:
// Background service worker for CSS Unused Rules Finder
chrome.runtime.onInstalled.addListener(() => {
console.log('CSS Unused Rules Finder extension installed');
});
// Handle messages from popup
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === 'getAnalysis') {
// Execute content script in active tab
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
if (tabs.length === 0) {
sendResponse({ error: 'No active tab found' });
return;
}
const tabId = tabs[0].id;
chrome.scripting.executeScript({
target: { tabId: tabId },
func: () => {
// This function runs in the context of the web page
// It performs the CSS analysis and returns results
const results = [];
const styleSheets = document.styleSheets;
for (let i = 0; i < styleSheets.length; i++) {
try {
const sheet = styleSheets[i];
const rules = sheet.cssRules || sheet.rules;
if (!rules) continue;
for (let j = 0; j < rules.length; j++) {
const rule = rules[j];
if (rule.type !== CSSRule.STYLE_RULE) continue;
const selectorText = rule.selectorText;
let isUsed = false;
try {
const selectors = selectorText.split(',').map(s => s.trim());
for (const sel of selectors) {
if (sel && document.querySelector(sel)) {
isUsed = true;
break;
}
}
} catch (e) {
isUsed = false;
}
results.push({
selector: selectorText,
cssText: rule.cssText,
isUsed: isUsed,
sheet: sheet.href || 'inline'
});
}
} catch (e) {
console.warn('Cannot access stylesheet:', e);
}
}
const totalRules = results.length;
const unusedRules = results.filter(r => !r.isUsed).length;
const unusedPercentage = totalRules > 0
? Math.round((unusedRules / totalRules) * 100)
: 0;
return {
totalRules,
unusedRules,
unusedPercentage,
details: results
};
}
}, (results) => {
if (chrome.runtime.lastError) {
sendResponse({ error: chrome.runtime.lastError.message });
} else {
sendResponse(results[0].result);
}
});
});
return true; // Keep message channel open for async response
}
});
The background script uses Chrome’s scripting API to execute code in the context of the active tab. This approach combines the analysis logic into a single function that runs directly in the web page, avoiding the need for a separate content script file.
Building the Popup Interface
The popup provides the user interface for the extension. Create popup.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CSS Unused Rules Finder</title>
<link rel="stylesheet" href="popup.css">
</head>
<body>
<div class="container">
<header>
<h1>CSS Unused Finder</h1>
<p class="subtitle">Find dead CSS in seconds</p>
</header>
<main id="app">
<div class="initial-state">
<button id="analyzeBtn" class="primary-btn">Analyze Page</button>
<p class="hint">Click to scan for unused CSS rules</p>
</div>
<div class="loading-state hidden">
<div class="spinner"></div>
<p>Analyzing stylesheets...</p>
</div>
<div class="results-state hidden">
<div class="summary">
<div class="stat-card">
<span class="stat-value" id="totalRules">0</span>
<span class="stat-label">Total Rules</span>
</div>
<div class="stat-card unused">
<span class="stat-value" id="unusedRules">0</span>
<span class="stat-label">Unused Rules</span>
</div>
<div class="stat-card percentage">
<span class="stat-value" id="unusedPercentage">0%</span>
<span class="stat-label">Unused</span>
</div>
</div>
<div class="actions">
<button id="copyBtn" class="secondary-btn">Copy Unused CSS</button>
<button id="exportBtn" class="secondary-btn">Export Report</button>
<button id="reanalyzeBtn" class="primary-btn">Re-analyze</button>
</div>
<div class="unused-list-container">
<h2>Unused Rules</h2>
<div id="unusedList" class="unused-list"></div>
</div>
</div>
<div class="error-state hidden">
<p class="error-message" id="errorMessage"></p>
<button id="retryBtn" class="primary-btn">Try Again</button>
</div>
</main>
</div>
<script src="popup.js"></script>
</body>
</html>
Styling the Popup
Create popup.css with attractive styling:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
width: 400px;
min-height: 300px;
background: #ffffff;
color: #333;
}
.container {
padding: 20px;
}
header {
text-align: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid #e0e0e0;
}
h1 {
font-size: 20px;
font-weight: 600;
color: #1a73e8;
}
.subtitle {
font-size: 13px;
color: #666;
margin-top: 4px;
}
.hidden {
display: none !important;
}
/* Buttons */
.primary-btn {
width: 100%;
padding: 12px 20px;
background: #1a73e8;
color: white;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: background 0.2s;
}
.primary-btn:hover {
background: #1557b0;
}
.primary-btn:active {
transform: scale(0.98);
}
.secondary-btn {
padding: 8px 16px;
background: #f1f3f4;
color: #333;
border: 1px solid #dadce0;
border-radius: 4px;
font-size: 13px;
cursor: pointer;
transition: background 0.2s;
}
.secondary-btn:hover {
background: #e8eaed;
}
.hint {
text-align: center;
font-size: 12px;
color: #888;
margin-top: 10px;
}
/* Loading State */
.loading-state {
text-align: center;
padding: 40px 20px;
}
.spinner {
width: 40px;
height: 40px;
border: 3px solid #f1f3f4;
border-top-color: #1a73e8;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 15px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Summary Stats */
.summary {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
margin-bottom: 20px;
}
.stat-card {
background: #f8f9fa;
padding: 15px 10px;
border-radius: 8px;
text-align: center;
}
.stat-card.unused {
background: #fef7e0;
}
.stat-card.percentage {
background: #e8f0fe;
}
.stat-value {
display: block;
font-size: 24px;
font-weight: 700;
color: #1a73e8;
}
.stat-card.unused .stat-value {
color: #f9ab00;
}
.stat-label {
display: block;
font-size: 11px;
color: #666;
margin-top: 4px;
}
/* Actions */
.actions {
display: flex;
gap: 8px;
margin-bottom: 20px;
}
.actions .primary-btn {
flex: 1;
}
.actions .secondary-btn {
flex: 1;
}
/* Unused List */
.unused-list-container {
max-height: 300px;
overflow-y: auto;
}
.unused-list-container h2 {
font-size: 14px;
font-weight: 600;
margin-bottom: 10px;
color: #333;
}
.unused-list {
border: 1px solid #e0e0e0;
border-radius: 6px;
overflow: hidden;
}
.unused-item {
padding: 10px;
border-bottom: 1px solid #e0e0e0;
font-size: 12px;
word-break: break-all;
}
.unused-item:last-child {
border-bottom: none;
}
.unused-item .selector {
font-family: 'Monaco', 'Menlo', monospace;
color: #d93025;
background: #fce8e6;
padding: 2px 6px;
border-radius: 3px;
}
.unused-item .source {
display: block;
color: #888;
margin-top: 4px;
font-size: 11px;
}
/* Error State */
.error-state {
text-align: center;
padding: 30px 20px;
}
.error-message {
color: #d93025;
margin-bottom: 15px;
font-size: 14px;
}
Popup JavaScript Logic
Create popup.js to handle user interactions:
// Popup script for CSS Unused Rules Finder
document.addEventListener('DOMContentLoaded', () => {
const analyzeBtn = document.getElementById('analyzeBtn');
const reanalyzeBtn = document.getElementById('reanalyzeBtn');
const copyBtn = document.getElementById('copyBtn');
const exportBtn = document.getElementById('exportBtn');
const retryBtn = document.getElementById('retryBtn');
const initialState = document.querySelector('.initial-state');
const loadingState = document.querySelector('.loading-state');
const resultsState = document.querySelector('.results-state');
const errorState = document.querySelector('.error-state');
let currentResults = null;
// Show loading state
function showLoading() {
initialState.classList.add('hidden');
resultsState.classList.add('hidden');
errorState.classList.add('hidden');
loadingState.classList.remove('hidden');
}
// Show results state
function showResults(results) {
loadingState.classList.add('hidden');
errorState.classList.add('hidden');
initialState.classList.add('hidden');
resultsState.classList.remove('hidden');
// Update stats
document.getElementById('totalRules').textContent = results.totalRules;
document.getElementById('unusedRules').textContent = results.unusedRules;
document.getElementById('unusedPercentage').textContent = results.unusedPercentage + '%';
// Populate unused rules list
const unusedList = document.getElementById('unusedList');
const unusedRules = results.details.filter(r => !r.isUsed);
if (unusedRules.length === 0) {
unusedList.innerHTML = '<div class="unused-item"><p>No unused CSS rules found!</p></div>';
} else {
unusedList.innerHTML = unusedRules.map(rule => `
<div class="unused-item">
<span class="selector">${escapeHtml(rule.selector)}</span>
<span class="source">${escapeHtml(rule.sheet)}</span>
</div>
`).join('');
}
currentResults = results;
}
// Show error state
function showError(message) {
loadingState.classList.add('hidden');
resultsState.classList.add('hidden');
initialState.classList.add('hidden');
errorState.classList.remove('hidden');
document.getElementById('errorMessage').textContent = message;
}
// Escape HTML for safe display
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Analyze page
function runAnalysis() {
showLoading();
chrome.runtime.sendMessage({ action: 'getAnalysis' }, (response) => {
if (chrome.runtime.lastError) {
showError(chrome.runtime.lastError.message);
return;
}
if (response.error) {
showError(response.error);
return;
}
showResults(response);
});
}
// Copy unused CSS to clipboard
function copyUnusedCSS() {
if (!currentResults) return;
const unusedRules = currentResults.details.filter(r => !r.isUsed);
const css = unusedRules.map(r => r.cssText).join('\n\n');
navigator.clipboard.writeText(css).then(() => {
const btn = document.getElementById('copyBtn');
const originalText = btn.textContent;
btn.textContent = 'Copied!';
setTimeout(() => {
btn.textContent = originalText;
}, 2000);
});
}
// Export report
function exportReport() {
if (!currentResults) return;
const unusedRules = currentResults.details.filter(r => !r.isUsed);
const report = {
date: new Date().toISOString(),
url: window.location.href,
summary: {
totalRules: currentResults.totalRules,
unusedRules: currentResults.unusedRules,
unusedPercentage: currentResults.unusedPercentage
},
unusedSelectors: unusedRules.map(r => ({
selector: r.selector,
cssText: r.cssText,
source: r.sheet
}))
};
const blob = new Blob([JSON.stringify(report, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'css-coverage-report.json';
a.click();
URL.revokeObjectURL(url);
}
// Event listeners
analyzeBtn.addEventListener('click', runAnalysis);
reanalyzeBtn.addEventListener('click', runAnalysis);
retryBtn.addEventListener('click', runAnalysis);
copyBtn.addEventListener('click', copyUnusedCSS);
exportBtn.addEventListener('click', exportReport);
});
Enhancing the Extension with Advanced Features
Now that we have a working extension, let’s explore ways to enhance it with more powerful features.
Supporting CSS Frameworks and Preprocessors
Many websites use CSS frameworks like Bootstrap, Tailwind, or custom preprocessing pipelines. Our basic implementation might flag framework utility classes as unused if they don’t appear in the current page but are part of a larger design system. Consider adding a feature that allows users to whitelist certain stylesheets or selectors.
Batch Analysis Across Multiple Pages
A single-page analysis has limitations because it only checks CSS rules against the current page state. Some CSS rules might be used on other pages of the site. Consider adding a crawling feature that visits multiple pages and aggregates unused CSS across the entire site.
Integration with Development Tools
For professional-grade tooling, consider adding features like browser storage for results history, keyboard shortcuts for quick analysis, and options to ignore specific selectors or patterns.
Testing Your Extension
Before publishing your extension, thorough testing ensures it works correctly across various websites.
Manual Testing
Load your extension in developer mode by navigating to chrome://extensions/, enabling “Developer mode,” and clicking “Load unpacked.” Test it on various types of websites including simple static sites, complex web applications, and sites with inline styles versus external stylesheets.
Handling Edge Cases
Pay attention to how your extension handles websites with dynamically generated content, Shadow DOM, iframes, and CSS-in-JS solutions. Each of these presents unique challenges for unused CSS detection.
Publishing Your Extension
Once you’ve thoroughly tested your extension, you can publish it to the Chrome Web Store. Prepare the following assets:
Your extension needs compelling icon graphics in the required sizes (16x16, 48x48, and 128x128 pixels). Write a clear, keyword-rich description that explains what your extension does and who it’s for. Consider creating a simple website or documentation page that explains how to use the extension effectively.
Conclusion
Building a CSS Unused Rules Finder Extension demonstrates how Chrome extension development can solve real-world problems. This tool addresses a genuine pain point for web developers: identifying and removing dead CSS that bloats stylesheets and slows down websites.
The extension we built leverages powerful browser APIs to analyze CSSOM and DOM, providing actionable insights about unused styles. By following the principles in this guide, you can customize and extend this foundation to create a production-quality tool that fits your specific workflow.
Remember that while our implementation provides solid basic functionality, there’s always room for improvement. Consider adding features like historical tracking, integration with version control systems, or automated reports that can help teams maintain clean stylesheets over time.
Start building today, and you’ll have a valuable addition to your developer toolkit in no time.
For more guides on Chrome extension development and building developer tools, explore our comprehensive documentation and tutorials.
Turn Your Extension Into a Business
Ready to monetize? The Extension Monetization Playbook covers freemium models, Stripe integration, subscription architecture, and growth strategies for Chrome extension developers.