LCP Subparts
A compact panel that breaks Largest Contentful Paint into its four canonical sub-parts — TTFB, Resource Load Delay, Resource Load, and Element Render Delay — and grades the total against the Core Web Vitals thresholds (good ≤ 2.5s, poor > 4s). Drop it into a CWV lesson, a perf audit, or any "why is the hero slow to paint?" walkthrough.
Preview
LCP breakdown
Total LCP is the sum of every sub-part — tune the sliders to see the verdict flip.
Total LCP5.00starget ≤ 2.5s
- TTFB400ms
- Resource Load Delay1800ms
- Resource Load2400ms
- Element Render Delay400ms
Customize
Sub-parts
400ms
1800ms
2400ms
400ms
Thresholds
2500ms
4000ms
Installation
npx shadcn@latest add https://craftbits.dev/r/lcp-subparts.jsonUsage
import { LcpSubparts } from "@craft-bits/core";
<LcpSubparts
title="Before optimisation"
subparts={[
{ id: "ttfb", label: "TTFB", ms: 400 },
{ id: "load-delay", label: "Resource Load Delay", ms: 1800 },
{ id: "load", label: "Resource Load", ms: 2400 },
{ id: "render-delay", label: "Element Render Delay", ms: 400 },
]}
/>Tighten the thresholds for an internal budget that's stricter than the public Core Web Vitals cut-off:
<LcpSubparts
subparts={subparts}
goodMs={2000}
poorMs={3500}
/>Anatomy
- Header. Optional
title(rendered with thecb-labelstyle) and adescriptionsub-line. Omit both for a chromeless panel. - Total readout. A single inset row pairs the literal "Total LCP" with the summed total in seconds and the target line. The total tone tracks the verdict (
good/needs-improvement/poor). - Sub-part rows. One row per entry in
subparts. Each row carries a label on the left, a proportional bar in the middle (width =msdivided by the total), and the millisecond value on the right. The bar tint mirrors the overall verdict so the whole breakdown reads as one bucket.
Understanding the component
- Total drives everything. The widget never asks for a separate
total— it sums everymsinsubparts. That way the bars always add up to the readout, and consumers can't accidentally drift the two. - Verdict bucket. The total is graded against
goodMs(default 2500ms) andpoorMs(default 4000ms).goodpaints--cb-success,needs-improvementpaints--cb-warning,poorpaints--cb-error. The wrapper exposesdata-cb-verdictso consumers can extend the palette without monkey-patching CSS. - Proportional bars. Each bar fills its share of the total. The widget guards against zero-total inputs so a freshly-constructed entry with all zeroes still renders a flat track instead of dividing by zero.
- Motion. Bar fills animate in once per value change with the
snapspring, and the animation short-circuits underprefers-reduced-motion— only the width transition runs, never an entrance flicker.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
subparts | LcpSubpart[] | required | Ordered list of sub-part contributions. |
title | ReactNode | — | Optional heading above the total. |
description | ReactNode | — | Optional sub-headline under the title. |
goodMs | number | 2500 | Threshold for the good verdict. |
poorMs | number | 4000 | Threshold above which the verdict reads as poor. |
hideTarget | boolean | false | Hide the target line in the total readout. |
headingAs | 'h2' | 'h3' | 'h4' | 'h3' | Tag for the title element. |
className | string | — | Merged onto the root via cn(). |
LcpSubpart
| Field | Type | Description |
|---|---|---|
id | string | Stable identifier. Surfaces via data-cb-sub. |
label | ReactNode | Display label (e.g. TTFB, Resource Load Delay). |
ms | number | Contribution of this sub-part to the total LCP, in milliseconds. |
Accessibility
- The wrapper is a
<section>withdata-cb-edu="lcp-subparts"anddata-cb-verdict="good" | "needs-improvement" | "poor"so consumers can extend the palette without monkey-patching CSS. - The total readout carries an
aria-labellikeTotal LCP 5.00s — poor, so screen-reader users get the verdict alongside the literal value. - Each row's bar is a
role="progressbar"witharia-valuenow/aria-valuemin/aria-valuemaxso the proportion is conveyed without depending on color, and the row exposesdata-cb-subfor tone hooks per sub-part. - The bar fill animations short-circuit under
prefers-reduced-motion.
Credits
- Extracted from:
terminal-dreams(src/components/frontend-design/perf-cwv/ui/LcpSubparts.tsx). The original was wired to a project-levelCwvContext, hard-coded a TD-specific subpart shape (base,valueMs,fix,fixLabel) and embedded a fix-toggle grid alongside the bars. This rewrite drops every context coupling and the fix grid — consumers pass a flatLcpSubpart[]and the widget visualises any LCP breakdown, not just the CWV lab's.