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 76msafter 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.json

Usage

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 the cb-label style) and description. Omit both for a chromeless panel.
  • Summary tiles. Four small chips — critical bytes, deferred bytes, above-the-fold coverage percent, and a critical / total rule count. Coverage tints bad when it drops below foucThreshold.
  • 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 the bad tone when coverage is too low to avoid FOUC.

Understanding the component

  1. Tier partition. Rules with critical: true flow into the inlined tier; the rest land in the async tier. The widget sums the bytes per tier and derives the timeline lengths from rttMs, msPerByte, and inlineMsPerByte.
  2. Render-blocking simulation. Before optimisation, FCP equals htmlParseMs + stylesheetDownload + cssParseMs. After optimisation, FCP equals htmlParseMs + criticalBytes × inlineMsPerByte — the async sheet keeps loading but no longer holds the paint.
  3. FOUC detection. When the share of critical: true rules drops below foucThreshold (default 0.85), the FCP marker switches to the bad tone 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.
  4. Motion. Each segment grows from zero on first paint with the SPRINGS.smooth token, the FCP marker pops in with SPRINGS.snap, and the saving line re-animates whenever the totals change. All animations short-circuit under prefers-reduced-motion.

Props

PropTypeDefaultDescription
rulesCriticalCssWidgetRule[]requiredOrdered list of CSS rules.
titleReactNodeOptional heading above the panel.
descriptionReactNodeOptional sub-headline under the title.
rttMsnumber40Simulated round-trip time.
msPerBytenumber0.6Multiplier from byte count to download time.
htmlParseMsnumber15Time the browser spends parsing the document head.
cssParseMsnumber8Time the browser spends parsing the external sheet.
inlineMsPerBytenumber0.8Cost per inlined critical byte.
foucThresholdnumber0.85Coverage below this flags the after frame as FOUC.
unitstring'KB'Display unit suffix for byte counts.
hideFooterbooleanfalseHide the FCP saving footer.
headingAs'h2' | 'h3' | 'h4''h3'Tag for the title element.
classNamestringMerged onto the root via cn().

CriticalCssWidgetRule

FieldTypeDescription
idstringStable identifier.
selectorReactNodeVisible rule label.
criticalbooleanWhen true, the rule lands in the inlined tier.
bytesnumberRule size in the widget's unit.

Accessibility

  • The wrapper is a <section> with data-cb-edu="critical-css-widget" and data-cb-fouc="true | false" mirroring the FOUC verdict.
  • Each timeline carries an aria-label like Before timeline, total 175ms so 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-label for 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-shape PerfContext (one criticalCssKB slider 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.