Wins Recap View
A compact recap primitive — a vertical list of before -> after wins. Each row carries a label, a before/after pair, a computed verdict (won / kept / lost), a percentage delta chip, and an optional caption. An optional banner slot sits above the list for the celebratory headline.
Preview
Optimisation recap
- First Contentful Paintverdict: Wonbefore 1840msafter 580msInlined critical CSS, deferred the rest.
- CSS payloadverdict: Wonbefore 86KBafter 38KBSafe audit dropped the unused selectors.
- Render-blocking requestsverdict: Wonbefore 4after 1Inlined the rest behind a single network round-trip.
- Cumulative Layout Shiftverdict: Wonbefore 0.12after 0.04Reserved space for fonts and hero image.
Customize
Options
Installation
npx shadcn@latest add https://craftbits.dev/r/wins-recap-view.jsonUsage
import { WinsRecapView } from "@craft-bits/core";
<WinsRecapView
wins={[
{ id: "fcp", label: "FCP", before: 1840, after: 580, unit: "ms" },
{ id: "bytes", label: "CSS payload", before: 86, after: 38, unit: "KB" },
{ id: "cls", label: "CLS", before: 0.12, after: 0.04 },
]}
/>Throughput-style metrics use direction: "higher" so a larger after counts as a win:
<WinsRecapView
wins={[
{ id: "rps", label: "Requests / sec", before: 120, after: 480, direction: "higher" },
{ id: "hit", label: "Cache hit rate", before: 0.42, after: 0.71, direction: "higher" },
]}
/>Use tolerance to express "holding the line is also a win" — useful for budgets:
<WinsRecapView
wins={[
{ id: "lcp", label: "LCP", before: 2.4, after: 2.5, unit: "s", tolerance: 0.2 },
]}
/>Headless usage of the engine — grade a single win without rendering:
import { computeWinRecap } from "@craft-bits/core";
const result = computeWinRecap({
id: "fcp",
label: "FCP",
before: 1840,
after: 580,
});
// result.verdict === "won"Anatomy
- Title — optional
cb-labelheading above the list. Hidden whentitle="". - Banner — optional success ribbon wrapped in
AnimatePresenceso callers can drop it in or pull it out as a goal flips. - Row — small rounded card with a verdict marker, a label, a verdict chip with percentage delta, the before -> after numbers, and an optional caption.
- Verdict marker — circular badge with a
won/kept/lostglyph. Tone matches the verdict. - Delta chip — uppercase pill carrying the verdict label plus the rounded percentage delta.
Understanding the component
- Direction. Each win declares whether smaller (
"lower", default) or larger ("higher") is better. The engine computes a normalised improvement so the same verdict logic works either way. - Tolerance. When
|after - before|is withintolerance, the row is gradedkept— neither a win nor a regression. Useful for budgets where holding the line is success. - Verdict.
wonwhen the improvement exceedstolerance.keptwhen withintolerance.lostotherwise. The verdict drives the marker, the chip tone, and the colour of theaftervalue. - Format. Each win can pass a
format(value, unit)callback to render numbers in a custom way (e.g. ms under 1000, s above). Defaults to value + unit. - Animation. Rows enter with a small staggered horizontal slide via spring
smooth. Theaftervalue re-animates on change via springsnap. The banner usesAnimatePresencefor clean in / out. - Reduced motion.
prefers-reduced-motion: reducesnaps every animation to its final frame.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
wins | WinsRecapWin[] | required | Ordered before/after rows. |
title | ReactNode | 'Wins recap' | Heading above the list. Pass "" to omit. |
banner | ReactNode | — | Celebratory banner above the list. |
headingAs | 'h2' | 'h3' | 'h4' | 'h3' | Tag for the title element. |
hideDetails | boolean | false | Hide per-win captions even if provided. |
aria-label | string | 'Wins recap' | Accessible label when no title is set. |
className | string | — | Merged onto the root via cn(). |
WinsRecapWin
| Field | Type | Description |
|---|---|---|
id | string | Stable identifier, used as React key. |
label | ReactNode | Visible label on top of the row. |
before | number | Pre-optimisation value. |
after | number | Post-optimisation value. |
unit | string | Optional unit suffix. |
direction | 'lower' | 'higher' | Which direction wins. Defaults to lower. |
tolerance | number | Absolute ties window — within this counts as kept. |
detail | ReactNode | Optional caption under the row. |
format | (value, unit) => string | Override the value rendering. |
Engine exports
| Export | Signature | Notes |
|---|---|---|
computeWinRecap | (win: WinsRecapWin) => WinsRecapComputed | Returns verdict, delta, pctDelta, absDelta, improvement. |
Accessibility
- The recap root is a
<section>with eitheraria-labeloraria-labelledby(whentitleis set). - The list is an
<ol role="list">ofrole="listitem"rows so assistive tech treats it as an ordered set of wins. aria-live="polite"plusaria-atomic="false"scopes update announcements to the changing row.- Each row carries
data-verdict="..."for consumer styling without monkey-patching CSS. - The before / after numbers are prefixed by visually-hidden
before/aftertext so the row reads cleanly without relying on the arrow glyph. - The banner is wrapped in
role="status"so screen readers announce it when it appears. - Reduced-motion users get static rows — no enter, no value transition, no banner spring.
Credits
- Extracted from:
terminal-dreams(src/components/frontend-design/perf-css/ui/WinsRecapView.tsx). The original was hard-coded to a CSS-perf lab — FCP, audit bytes, broken JS counters,@layerandcontent-visibilitytoggles all baked into the prop surface and the verdict logic. This rewrite generalises the recap to an arbitrary list of before/after wins, computes the verdict from each row'sdirection+tolerance, and exposescomputeWinRecapas a named export so consumers can grade a result without rendering.