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
  • TTFB
    400ms
  • Resource Load Delay
    1800ms
  • Resource Load
    2400ms
  • Element Render Delay
    400ms
Customize
Sub-parts
400ms
1800ms
2400ms
400ms
Thresholds
2500ms
4000ms

Installation

npx shadcn@latest add https://craftbits.dev/r/lcp-subparts.json

Usage

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 the cb-label style) and a description sub-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 = ms divided 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

  1. Total drives everything. The widget never asks for a separate total — it sums every ms in subparts. That way the bars always add up to the readout, and consumers can't accidentally drift the two.
  2. Verdict bucket. The total is graded against goodMs (default 2500ms) and poorMs (default 4000ms). good paints --cb-success, needs-improvement paints --cb-warning, poor paints --cb-error. The wrapper exposes data-cb-verdict so consumers can extend the palette without monkey-patching CSS.
  3. 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.
  4. Motion. Bar fills animate in once per value change with the snap spring, and the animation short-circuits under prefers-reduced-motion — only the width transition runs, never an entrance flicker.

Props

PropTypeDefaultDescription
subpartsLcpSubpart[]requiredOrdered list of sub-part contributions.
titleReactNodeOptional heading above the total.
descriptionReactNodeOptional sub-headline under the title.
goodMsnumber2500Threshold for the good verdict.
poorMsnumber4000Threshold above which the verdict reads as poor.
hideTargetbooleanfalseHide the target line in the total readout.
headingAs'h2' | 'h3' | 'h4''h3'Tag for the title element.
classNamestringMerged onto the root via cn().

LcpSubpart

FieldTypeDescription
idstringStable identifier. Surfaces via data-cb-sub.
labelReactNodeDisplay label (e.g. TTFB, Resource Load Delay).
msnumberContribution of this sub-part to the total LCP, in milliseconds.

Accessibility

  • The wrapper is a <section> with data-cb-edu="lcp-subparts" and data-cb-verdict="good" | "needs-improvement" | "poor" so consumers can extend the palette without monkey-patching CSS.
  • The total readout carries an aria-label like Total LCP 5.00s — poor, so screen-reader users get the verdict alongside the literal value.
  • Each row's bar is a role="progressbar" with aria-valuenow / aria-valuemin / aria-valuemax so the proportion is conveyed without depending on color, and the row exposes data-cb-sub for 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-level CwvContext, 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 flat LcpSubpart[] and the widget visualises any LCP breakdown, not just the CWV lab's.