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

  1. First Contentful Paintverdict: Won
    before 1840msafter 580ms
    Inlined critical CSS, deferred the rest.
  2. CSS payloadverdict: Won
    before 86KBafter 38KB
    Safe audit dropped the unused selectors.
  3. Render-blocking requestsverdict: Won
    before 4after 1
    Inlined the rest behind a single network round-trip.
  4. Cumulative Layout Shiftverdict: Won
    before 0.12after 0.04
    Reserved space for fonts and hero image.
Customize
Options

Installation

npx shadcn@latest add https://craftbits.dev/r/wins-recap-view.json

Usage

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-label heading above the list. Hidden when title="".
  • Banner — optional success ribbon wrapped in AnimatePresence so 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 / lost glyph. Tone matches the verdict.
  • Delta chip — uppercase pill carrying the verdict label plus the rounded percentage delta.

Understanding the component

  1. 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.
  2. Tolerance. When |after - before| is within tolerance, the row is graded kept — neither a win nor a regression. Useful for budgets where holding the line is success.
  3. Verdict. won when the improvement exceeds tolerance. kept when within tolerance. lost otherwise. The verdict drives the marker, the chip tone, and the colour of the after value.
  4. 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.
  5. Animation. Rows enter with a small staggered horizontal slide via spring smooth. The after value re-animates on change via spring snap. The banner uses AnimatePresence for clean in / out.
  6. Reduced motion. prefers-reduced-motion: reduce snaps every animation to its final frame.

Props

PropTypeDefaultDescription
winsWinsRecapWin[]requiredOrdered before/after rows.
titleReactNode'Wins recap'Heading above the list. Pass "" to omit.
bannerReactNodeCelebratory banner above the list.
headingAs'h2' | 'h3' | 'h4''h3'Tag for the title element.
hideDetailsbooleanfalseHide per-win captions even if provided.
aria-labelstring'Wins recap'Accessible label when no title is set.
classNamestringMerged onto the root via cn().

WinsRecapWin

FieldTypeDescription
idstringStable identifier, used as React key.
labelReactNodeVisible label on top of the row.
beforenumberPre-optimisation value.
afternumberPost-optimisation value.
unitstringOptional unit suffix.
direction'lower' | 'higher'Which direction wins. Defaults to lower.
tolerancenumberAbsolute ties window — within this counts as kept.
detailReactNodeOptional caption under the row.
format(value, unit) => stringOverride the value rendering.

Engine exports

ExportSignatureNotes
computeWinRecap(win: WinsRecapWin) => WinsRecapComputedReturns verdict, delta, pctDelta, absDelta, improvement.

Accessibility

  • The recap root is a <section> with either aria-label or aria-labelledby (when title is set).
  • The list is an <ol role="list"> of role="listitem" rows so assistive tech treats it as an ordered set of wins.
  • aria-live="polite" plus aria-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 / after text 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, @layer and content-visibility toggles 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's direction + tolerance, and exposes computeWinRecap as a named export so consumers can grade a result without rendering.