Critical CSS Widget
A compact panel that visualises the critical CSS optimisation. Pass in a flat list of CSS rules tagged with critical: boolean; the widget partitions them into an above-the-fold tier (inlined into the document head) and a below-the-fold tier (loaded asynchronously), sums each tier's bytes, and renders two render-blocking timelines — one for the unoptimised stylesheet flow, one for the inlined-critical flow. A footer prints the FCP saving.
Preview
Critical CSS audit
Toggle hero or card to drop above-the-fold coverage and watch FOUC trigger.
- Critical
- 3.90 KB
- Deferred
- 17.3 KB
- Coverage
- 50%
- Rules
- 4/8
Before
FCP @ 76ms
After
FCP @ 18ms · FOUC
- :root, body0.40 KB
- h1, h2, p0.90 KB
- .nav1.20 KB
- .hero1.40 KB
- .card3.20 KB
- .modal5.60 KB
- .footer2.10 KB
- .cta-strip6.40 KB
FCPbefore 76ms→after 18mssaved 58ms
Customize
CSS rule sizes (KB)
1.2KB
1.4KB
3.2KB
5.6KB
Network simulation
40ms
0.6
Coverage toggles
Installation
npx shadcn@latest add https://craftbits.dev/r/critical-css-widget.jsonUsage
import { CriticalCssWidget } from "@craft-bits/core";
<CriticalCssWidget
title="Critical CSS audit"
rules={[
{ id: "reset", selector: ":root, body", critical: true, bytes: 0.4 },
{ id: "nav", selector: ".nav", critical: true, bytes: 1.2 },
{ id: "hero", selector: ".hero", critical: true, bytes: 1.4 },
{ id: "modal", selector: ".modal", critical: false, bytes: 5.6 },
{ id: "footer", selector: ".footer", critical: false, bytes: 2.1 },
]}
/>Tune the simulated network — slow connections amplify the win:
<CriticalCssWidget
rules={rules}
rttMs={250}
msPerByte={1.2}
/>Anatomy
- Header. Optional
title(rendered with thecb-labelstyle) anddescription. Omit both for a chromeless panel. - Summary tiles. Four small chips — critical bytes, deferred bytes, above-the-fold coverage percent, and a
critical / totalrule count. Coverage tintsbadwhen it drops belowfoucThreshold. - Before timeline. HTML parse → external stylesheet download → CSS parse. The marker sits at the FCP that resolves once the stylesheet finishes.
- After timeline. HTML + inlined critical CSS (FCP marker lands here) → asynchronous stylesheet download in the background. The async segment paints muted because it no longer blocks rendering.
- Rule list. One row per rule. Critical rules sport an accent dot and full opacity; deferred rules render muted.
- Footer. Before-vs-after FCP, with the saving formatted as
saved 42ms. Switches to thebadtone when coverage is too low to avoid FOUC.
Understanding the component
- Tier partition. Rules with
critical: trueflow into the inlined tier; the rest land in the async tier. The widget sums the bytes per tier and derives the timeline lengths fromrttMs,msPerByte, andinlineMsPerByte. - Render-blocking simulation. Before optimisation, FCP equals
htmlParseMs + stylesheetDownload + cssParseMs. After optimisation, FCP equalshtmlParseMs + criticalBytes × inlineMsPerByte— the async sheet keeps loading but no longer holds the paint. - FOUC detection. When the share of
critical: truerules drops belowfoucThreshold(default0.85), the FCP marker switches to thebadtone and the coverage chip turns red — a hint that the inlined CSS isn't covering enough of the above-the-fold tree to prevent a flash of unstyled content. - Motion. Each segment grows from zero on first paint with the
SPRINGS.smoothtoken, the FCP marker pops in withSPRINGS.snap, and the saving line re-animates whenever the totals change. All animations short-circuit underprefers-reduced-motion.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
rules | CriticalCssWidgetRule[] | required | Ordered list of CSS rules. |
title | ReactNode | — | Optional heading above the panel. |
description | ReactNode | — | Optional sub-headline under the title. |
rttMs | number | 40 | Simulated round-trip time. |
msPerByte | number | 0.6 | Multiplier from byte count to download time. |
htmlParseMs | number | 15 | Time the browser spends parsing the document head. |
cssParseMs | number | 8 | Time the browser spends parsing the external sheet. |
inlineMsPerByte | number | 0.8 | Cost per inlined critical byte. |
foucThreshold | number | 0.85 | Coverage below this flags the after frame as FOUC. |
unit | string | 'KB' | Display unit suffix for byte counts. |
hideFooter | boolean | false | Hide the FCP saving footer. |
headingAs | 'h2' | 'h3' | 'h4' | 'h3' | Tag for the title element. |
className | string | — | Merged onto the root via cn(). |
CriticalCssWidgetRule
| Field | Type | Description |
|---|---|---|
id | string | Stable identifier. |
selector | ReactNode | Visible rule label. |
critical | boolean | When true, the rule lands in the inlined tier. |
bytes | number | Rule size in the widget's unit. |
Accessibility
- The wrapper is a
<section>withdata-cb-edu="critical-css-widget"anddata-cb-fouc="true | false"mirroring the FOUC verdict. - Each timeline carries an
aria-labellikeBefore timeline, total 175msso screen readers announce the before/after relationship. - The bar segments themselves are decorative (
aria-hidden) — the rule list under the timelines is the source of truth for assistive tech. - The summary tiles include an
aria-labelfor the coverage chip so the percent is conveyed without depending on colour. - Animations short-circuit under
prefers-reduced-motion.
Credits
- Extracted from:
terminal-dreams(src/components/frontend-design/sdp-web-performance/ui/CriticalCSSWidget.tsx). The original was wired to a fixed-shapePerfContext(onecriticalCssKBslider against a hardcoded 48 KB sheet, with FOUC flagged below 3 KB). This rewrite drops the context dependency and the single-slider cap — consumers pass an arbitrary list of{ id, selector, critical, bytes }rules so the widget can model any CSS inventory, computes the FOUC verdict from above-the-fold coverage rather than absolute kilobyte counts, and exposes the network simulation knobs (rttMs,msPerByte,inlineMsPerByte) as props.