Claude
Skills
Sign in
Back

css-coverage-treemap

Included with Lifetime
$97 forever

Collect CSS rule usage via Chrome DevTools Protocol (CDP) and report per-stylesheet coverage with unused selector analysis.

Web Dev

What this skill does


# CSS Coverage Treemap

Use the Chrome DevTools Protocol (CDP) to track which CSS rules are actually used during page rendering and user interaction. Produces a per-stylesheet breakdown of used vs. unused rules, identifies the top unused selectors, and estimates potential file size savings.

## When to Use

- Identifying dead CSS that can be safely removed to reduce page weight
- Auditing CSS frameworks (Bootstrap, Tailwind utility classes) for unused rules
- Measuring CSS coverage before and after a refactoring effort
- Estimating transfer size savings from CSS tree-shaking
- Comparing CSS usage across different page routes in a single-page application

## Prerequisites

- Playwright MCP server connected with a **Chromium** browser session (CDP required)
- Target page must be accessible in the browser session
- For accurate coverage, the page should be interacted with to trigger dynamic CSS (hover states, modals, accordions, media queries)

## Workflow

### Step 1: Start CSS Coverage Tracking via CDP

Use `browser_run_code` to create a CDP session and begin tracking CSS rule usage. This must run **before** navigating to the page so that all stylesheets are captured from initial load.

```javascript
browser_run_code({
  code: `async (page) => {
    const client = await page.context().newCDPSession(page);

    // Store CDP client for later use
    page.__cdpClient = client;

    // Enable CSS domain and start tracking rule usage
    await client.send('CSS.enable');
    await client.send('CSS.startRuleUsageTracking');

    return 'CSS coverage tracking started';
  }`
})
```

### Step 2: Navigate to the Target Page

```
browser_navigate({ url: "https://example.com/page" })
```

Wait for the page to fully load including stylesheets:

```
browser_wait_for({ time: 3 })
```

Or wait for a specific element that indicates the page is ready:

```
browser_wait_for({ text: "some content" })
```

### Step 3: Exercise Dynamic CSS

Interact with the page to trigger CSS rules that only apply during user interactions. This step is critical for accurate coverage -- without it, hover styles, modal styles, and animation classes will appear as unused.

Use a combination of MCP tools to simulate interactions:

**Hover over navigation menus:**

```
browser_snapshot()
```

Then identify menu elements and hover:

```
browser_hover({ ref: "menu-ref", element: "Navigation menu" })
browser_wait_for({ time: 1 })
```

**Open modals or dialogs:**

```
browser_click({ ref: "modal-trigger-ref", element: "Open modal button" })
browser_wait_for({ time: 1 })
browser_press_key({ key: "Escape" })
```

**Expand accordions or collapsible sections:**

```
browser_click({ ref: "accordion-ref", element: "Accordion header" })
browser_wait_for({ time: 0.5 })
```

**Scroll to trigger lazy-loaded components and scroll-based styles:**

```javascript
browser_run_code({
  code: `async (page) => {
    await page.evaluate(() => window.scrollTo(0, document.documentElement.scrollHeight));
    await page.waitForTimeout(1500);
    await page.evaluate(() => window.scrollTo(0, 0));
    await page.waitForTimeout(500);
    return 'Scroll complete';
  }`
})
```

### Step 4: Stop Tracking and Collect Rule Usage

Use `browser_run_code` to stop the tracking and retrieve the raw rule usage data.

```javascript
browser_run_code({
  code: `async (page) => {
    const client = page.__cdpClient;

    const { ruleUsage } = await client.send('CSS.stopRuleUsageTracking');

    // Store the raw data for analysis
    await page.evaluate((data) => {
      window.__cssRuleUsage = data;
    }, ruleUsage);

    return {
      totalRules: ruleUsage.length,
      usedRules: ruleUsage.filter(r => r.used).length,
      unusedRules: ruleUsage.filter(r => !r.used).length
    };
  }`
})
```

### Step 5: Enumerate Stylesheets and Map Rules

Use `browser_evaluate` to correlate the CDP rule usage data with actual stylesheet information from the DOM.

```javascript
browser_evaluate({
  function: `() => {
    const sheets = Array.from(document.styleSheets);
    const sheetData = [];

    sheets.forEach((sheet, sheetIndex) => {
      try {
        const href = sheet.href || ('inline-style-' + sheetIndex);
        const shortName = href.includes('/')
          ? href.split('/').pop().split('?')[0]
          : href;

        let rules;
        try {
          rules = Array.from(sheet.cssRules || []);
        } catch (e) {
          // Cross-origin stylesheet, cannot read rules
          sheetData.push({
            href: href,
            shortName: shortName,
            crossOrigin: true,
            totalRules: 'unknown',
            error: 'Cannot read cross-origin stylesheet'
          });
          return;
        }

        const ruleDetails = rules.map((rule, ruleIndex) => {
          let selector = '';
          let type = rule.type;
          let typeName = 'unknown';

          switch (rule.type) {
            case 1: // CSSStyleRule
              selector = rule.selectorText || '';
              typeName = 'style';
              break;
            case 3: // CSSImportRule
              typeName = 'import';
              break;
            case 4: // CSSMediaRule
              selector = '@media ' + rule.conditionText;
              typeName = 'media';
              break;
            case 5: // CSSFontFaceRule
              typeName = 'font-face';
              break;
            case 7: // CSSKeyframesRule
              selector = '@keyframes ' + rule.name;
              typeName = 'keyframes';
              break;
            case 12: // CSSSupportsRule
              selector = '@supports ' + rule.conditionText;
              typeName = 'supports';
              break;
            default:
              typeName = 'type-' + rule.type;
          }

          return {
            index: ruleIndex,
            selector: selector.substring(0, 120),
            typeName: typeName,
            cssText: rule.cssText ? rule.cssText.substring(0, 200) : ''
          };
        });

        sheetData.push({
          href: href,
          shortName: shortName,
          crossOrigin: false,
          totalRules: rules.length,
          rules: ruleDetails
        });
      } catch (e) {
        sheetData.push({
          href: sheet.href || 'unknown',
          error: e.message
        });
      }
    });

    return { sheets: sheetData, totalSheets: sheets.length };
  }`
})
```

### Step 6: Compute Coverage Analysis

Use `browser_evaluate` to merge the CDP rule usage data with the stylesheet enumeration and produce the final analysis.

```javascript
browser_evaluate({
  function: `() => {
    const ruleUsage = window.__cssRuleUsage || [];

    // Aggregate by stylesheet
    const byStylesheet = {};
    ruleUsage.forEach(rule => {
      const key = rule.styleSheetId;
      if (!byStylesheet[key]) {
        byStylesheet[key] = { used: 0, unused: 0, total: 0, rules: [] };
      }
      byStylesheet[key].total++;
      if (rule.used) {
        byStylesheet[key].used++;
      } else {
        byStylesheet[key].unused++;
      }
    });

    const totalUsed = ruleUsage.filter(r => r.used).length;
    const totalUnused = ruleUsage.filter(r => !r.used).length;
    const totalRules = ruleUsage.length;
    const usagePercent = totalRules > 0
      ? Math.round((totalUsed / totalRules) * 1000) / 10
      : 0;

    // Estimate savings from performance.getEntriesByType
    const cssResources = performance.getEntriesByType('resource')
      .filter(r => r.initiatorType === 'link' || r.name.endsWith('.css'));
    const totalCSSBytes = cssResources.reduce((sum, r) => sum + (r.transferSize || 0), 0);
    const estimatedSavingsBytes = Math.round(totalCSSBytes * (totalUnused / Math.max(totalRules, 1)));

    // Per-stylesheet summary
    const stylesheetSummary = Object.entries(byStylesheet).map(([id, data]) => ({
      styleSheetId: id,
      used: data.used,
      unused: data.unused,
      total: data.total,
      usagePercent: Math.round((data.used / 

Related in Web Dev