Unused CSS View

A compact panel that visualises a stylesheet's coverage report. Pass in a flat list of selectors tagged with used: boolean; the widget partitions them into used vs unused tiers, sums each tier's bytes, renders a coverage meter, and prints a per-selector breakdown with bars proportional to byte cost. The summary tints bad when coverage drops below the threshold — the signal that a purge pass would meaningfully shrink the bundle.

Preview

Stylesheet coverage

DevTools coverage report — toggle below to flip rules between used and dead.

Used
7.10 KB
Unused
15.5 KB
Coverage
31%
Selectors
5/9
used 7.10 KBunused 15.5 KB
  • :root, body
    0.40 KB
  • h1, h2, p
    0.90 KB
  • .nav, .nav__link
    1.40 KB
  • .hero, .hero__cta
    1.60 KB
  • .card, .card--featured
    2.80 KB
  • .carousel, .carousel__dot
    3.60 KB
  • .modal, .modal__backdrop
    5.60 KB
  • .grid-12, .grid-12 > *
    4.20 KB
  • .btn-legacy
    2.10 KB
Customize
Selector sizes (KB)
1.4KB
1.6KB
2.8KB
5.6KB
4.2KB
3.6KB
Coverage flips
0.5
Chrome

Installation

npx shadcn@latest add https://craftbits.dev/r/unused-css-view.json

Usage

import { UnusedCssView, type UnusedCssViewSelector } from "@craft-bits/core";
 
const selectors: UnusedCssViewSelector[] = [
  { id: "reset", selector: ":root, body", used: true, bytes: 0.4 },
  { id: "nav", selector: ".nav", used: true, bytes: 1.4 },
  { id: "hero", selector: ".hero", used: true, bytes: 1.6 },
  { id: "modal", selector: ".modal", used: false, bytes: 5.6 },
  { id: "legacy-grid", selector: ".grid-12", used: false, bytes: 4.2 },
];
 
export function Demo() {
  return (
    <UnusedCssView
      title="Stylesheet coverage"
      selectors={selectors}
    />
  );
}

Cap the list to surface the heaviest unused rules first — the candidates that pay back the most bytes per deletion:

<UnusedCssView
  selectors={selectors}
  maxRows={5}
  goodCoverageThreshold={0.6}
/>

Anatomy

  • Header. Optional title (rendered with the cb-label style) and description. Omit both for a chromeless panel.
  • Summary tiles. Four small chips — used bytes, unused bytes, coverage percent, and a used/total selector count. The coverage chip tints bad when it drops below the threshold.
  • Coverage meter. A short rail that grows to the used-byte share. Paints cb-success when coverage clears the threshold; otherwise mutes so the panel reads as "purge candidate".
  • Selector list. One row per selector. Used selectors carry an accent dot and an accent-tinted bar; unused selectors render muted. Bar widths are proportional to the heaviest row in the visible window.

Understanding the component

  1. Tier partition. Selectors with used: true flow into the used tier; the rest land in the unused tier. The widget sums bytes per tier and derives the coverage ratio from used bytes over total bytes.
  2. Low-coverage verdict. When coverage drops below goodCoverageThreshold (default 0.5), the summary tints bad, the meter mutes, and the root carries data-cb-coverage="low" — a styling hook for consumers that want to flag the panel.
  3. Bar scale. Bars are scaled against the largest byte count in the input list, not against the full width. That keeps a single heavy outlier from collapsing every other row to a sliver.
  4. Row capping. Set maxRows to surface only the heaviest entries — the widget re-sorts by bytes descending before slicing, so the most rewarding deletions surface at the top.
  5. Motion. The coverage meter and the per-row bars grow from zero on first paint with the SPRINGS.smooth token. All animations short-circuit under prefers-reduced-motion.

Props

PropTypeDefaultDescription
selectorsUnusedCssViewSelector[]requiredOrdered list of selectors from the coverage report.
titleReactNodeOptional heading above the panel.
descriptionReactNodeOptional sub-headline under the title.
goodCoverageThresholdnumber0.5Coverage below this tints the summary bad.
unitstring'KB'Display unit suffix for byte counts.
hideListbooleanfalseHide the per-selector list.
hideSummarybooleanfalseHide the summary tiles row.
maxRowsnumberCap the visible list; rows re-sorted bytes-desc before slicing.
headingAs'h2' | 'h3' | 'h4''h3'Tag for the title element.
classNamestringMerged onto the root via cn().

UnusedCssViewSelector

FieldTypeDescription
idstringStable identifier.
selectorReactNodeVisible selector label.
usedbooleantrue when the selector matched at least one node during the coverage run.
bytesnumberRule size in the widget's unit.

Accessibility

  • The wrapper is a <section> with data-cb-edu="unused-css-view" and data-cb-coverage="low | good" mirroring the verdict.
  • The coverage meter carries an aria-label reading "Coverage meter — N percent used" so screen readers convey the verdict without depending on colour.
  • The summary tiles include an aria-label for the coverage chip so the percent is conveyed independently.
  • Each selector row carries an aria-label of the form "Unused — 5.6 KB" so the tier and the byte cost are announced.
  • The bars themselves are decorative (aria-hidden) — the row labels are the source of truth for assistive tech.
  • Animations short-circuit under prefers-reduced-motion.

Credits

  • Extracted from: terminal-dreams (src/components/frontend-design/perf-css/ui/UnusedCSSView.tsx). The original was wired to the css-perf-simulator engine — a CSSRuleMock shape that carried matchCount, toggledByJS, jsReason, and breaks fields, plus an interactive "judge each rule then commit the audit" UI that flagged JS-toggled rules the user incorrectly deleted. This rewrite drops the simulator coupling and the judgment workflow, keeps only the coverage-report half of the visualisation, and exposes a generic { id, selector, used, bytes } shape so any DevTools coverage export can flow straight in. The summary tiles and meter are new — the original used a free-form "bytes panel" tied to its judgment state.