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, body0.40 KB
- h1, h2, p0.90 KB
- .nav, .nav__link1.40 KB
- .hero, .hero__cta1.60 KB
- .card, .card--featured2.80 KB
- .carousel, .carousel__dot3.60 KB
- .modal, .modal__backdrop5.60 KB
- .grid-12, .grid-12 > *4.20 KB
- .btn-legacy2.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.jsonUsage
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 thecb-labelstyle) anddescription. 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
badwhen it drops below the threshold. - Coverage meter. A short rail that grows to the used-byte share. Paints
cb-successwhen 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
- Tier partition. Selectors with
used: trueflow 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. - Low-coverage verdict. When coverage drops below
goodCoverageThreshold(default0.5), the summary tintsbad, the meter mutes, and the root carriesdata-cb-coverage="low"— a styling hook for consumers that want to flag the panel. - 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.
- Row capping. Set
maxRowsto surface only the heaviest entries — the widget re-sorts by bytes descending before slicing, so the most rewarding deletions surface at the top. - Motion. The coverage meter and the per-row bars grow from zero on first paint with the
SPRINGS.smoothtoken. All animations short-circuit underprefers-reduced-motion.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
selectors | UnusedCssViewSelector[] | required | Ordered list of selectors from the coverage report. |
title | ReactNode | — | Optional heading above the panel. |
description | ReactNode | — | Optional sub-headline under the title. |
goodCoverageThreshold | number | 0.5 | Coverage below this tints the summary bad. |
unit | string | 'KB' | Display unit suffix for byte counts. |
hideList | boolean | false | Hide the per-selector list. |
hideSummary | boolean | false | Hide the summary tiles row. |
maxRows | number | — | Cap the visible list; rows re-sorted bytes-desc before slicing. |
headingAs | 'h2' | 'h3' | 'h4' | 'h3' | Tag for the title element. |
className | string | — | Merged onto the root via cn(). |
UnusedCssViewSelector
| Field | Type | Description |
|---|---|---|
id | string | Stable identifier. |
selector | ReactNode | Visible selector label. |
used | boolean | true when the selector matched at least one node during the coverage run. |
bytes | number | Rule size in the widget's unit. |
Accessibility
- The wrapper is a
<section>withdata-cb-edu="unused-css-view"anddata-cb-coverage="low | good"mirroring the verdict. - The coverage meter carries an
aria-labelreading "Coverage meter — N percent used" so screen readers convey the verdict without depending on colour. - The summary tiles include an
aria-labelfor the coverage chip so the percent is conveyed independently. - Each selector row carries an
aria-labelof 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 thecss-perf-simulatorengine — aCSSRuleMockshape that carriedmatchCount,toggledByJS,jsReason, andbreaksfields, 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.