Video Strategy Board
A side-by-side video-loading comparison board. Pass a flat list of strategies — autoplay, poster-only, lazy facade, preload-metadata, or anything else — and the board paints a card per strategy with the two numbers that actually matter: bytes the page pays before the user clicks play, and time-to-first-paint of the video frame once they do.
Video loading strategies
Compare the four common video-loading approaches across initial cost and on-click latency.
- AutoplayRaw <video src> with autoplay
- Bytes until play
- 8.5 MB
- TTFP on click
- 0ms
8.5 MB of MP4 begins downloading before HTML parsing finishes. Bandwidth contention pushes LCP back ~2.8s on slow 4G.
<video src="hero.mp4" autoplay muted playsinline /> - Poster onlyPoster image + click to play
- Bytes until play
- 45 KB
- TTFP on click
- 1.8s
45 KB poster JPEG is the only network cost. The full stream begins on click — expect ~1.8s before the first frame paints.
<video poster="hero.jpg" preload="none" controls /> - LazyFacade — upgrade to full player on click
- Bytes until play
- 35 KB
- TTFP on click
- 2.0s
32 KB poster + 3 KB upgrade JS. The full IFrame and player JS (~1.2 MB) only fetches when the reader clicks.
<lite-youtube videoid="dQw4w9WgXcQ" playlabel="Play" /> - Preload metadataPoster + preload metadatabest pick
- Bytes until play
- 78 KB
- TTFP on click
- 120ms
Poster JPEG (45 KB) plus a 33 KB metadata fetch — enough to render the frame and duration. Cheap on first paint, ~120ms perceived stall on click.
<video poster="hero.jpg" preload="metadata" controls />
Installation
npx shadcn@latest add https://craftbits.dev/r/video-strategy-board.jsonUsage
import { VideoStrategyBoard } from "@craft-bits/core";
<VideoStrategyBoard
strategies={[
{ id: "autoplay", label: "Autoplay MP4", bytesUntilPlay: 8500, ttfp: 0 },
{ id: "poster", label: "Poster + click", bytesUntilPlay: 45, ttfp: 1800 },
{ id: "lazy", label: "Facade", bytesUntilPlay: 35, ttfp: 2000 },
{ id: "metadata", label: "Preload metadata", bytesUntilPlay: 78, ttfp: 120 },
]}
bestStrategyId="metadata"
/>With richer per-strategy content (short tag, tradeoff prose, code sample):
<VideoStrategyBoard
title="Above-the-fold hero video"
strategies={[
{
id: "metadata",
shortLabel: "Poster + metadata",
label: "Poster JPEG + preload metadata",
bytesUntilPlay: 78,
ttfp: 120,
tradeoff: "Cheap on first paint, ~120ms perceived stall on click.",
code: '<video poster="hero.jpg" preload="metadata" controls />',
},
]}
bestStrategyId="metadata"
/>Anatomy
- Header. Optional
titleanddescription. Both omittable for a chromeless board. - Strategy card. One per entry in
strategies. Carries the strategy name, an optionalshortLabeltag, a two-cell metric strip (bytes until play / TTFP on click), an optional tradeoff line, and an optional code sample. - Best-pick badge. When
bestStrategyIdmatches the card'sid, the card picks up an accent border and a "best pick" pill in the top-right. - Verdict tone. Each metric cell is coloured against the configured thresholds — green under
good, red at/overbad, orange in between.
Understanding the component
- Two numbers, that's it. Every strategy reports
bytesUntilPlay(the cost the page pays before any click) andttfp(the wait once the user clicks play). Both feed the metric strip and the per-cell verdict. - Thresholds drive tone.
bytesUntilPlayThresholdsandttfpThresholdseach have agoodandbadcutoff. Values at or undergoodrategood, values at or overbadratebad, everything in between rateswarn. The cell paintscb-success/cb-warning/cb-erroraccordingly. - Best-pick highlight.
bestStrategyIdis a free-form opt-in — match it to any card'sidand that card getsdata-cb-best="true", an accent border, and a pill. Omit it entirely if you want a flat comparison with no recommendation. - Grid scaling. The card grid is one column at the smallest breakpoint, two at
sm, three atlgwhen there are at least three strategies, four atxlwhen there are at least four. Anything beyond four wraps onto a second row. - Motion. Each card fades and rises in once on mount (spring
snap). Underprefers-reduced-motion, the enter animation is skipped — cards render in place.
Variants
// Strict budget — anything over 50KB is already bad.
<VideoStrategyBoard
strategies={strategies}
bytesUntilPlayThresholds={{ good: 30, bad: 50 }}
ttfpThresholds={{ good: 100, bad: 500 }}
/>
// Compact — hide the code-sample blocks for embedded comparisons.
<VideoStrategyBoard strategies={strategies} hideCode />
// No tradeoff prose — pure metric comparison.
<VideoStrategyBoard strategies={strategies} hideTradeoff />Props
| Prop | Type | Default | Description |
|---|---|---|---|
strategies | VideoStrategyBoardStrategy[] | required | Strategies to compare. At least one. |
title | ReactNode | — | Optional heading above the comparison grid. |
description | ReactNode | — | Optional sub-headline under the title. |
bestStrategyId | string | — | id of the recommended strategy. The matching card picks up the accent border and the "best pick" pill. |
bytesUntilPlayThresholds | { good: number; bad: number } | { good: 100, bad: 1000 } | Initial-cost thresholds in kilobytes. |
ttfpThresholds | { good: number; bad: number } | { good: 300, bad: 1500 } | TTFP thresholds in milliseconds. |
hideCode | boolean | false | Hide the code-sample block under each card. |
hideTradeoff | boolean | false | Hide the tradeoff prose line. |
headingAs | 'h2' | 'h3' | 'h4' | 'h3' | Tag for the title element. |
className | string | — | Merged onto the root via cn(). |
VideoStrategyBoardStrategy
| Field | Type | Description |
|---|---|---|
id | string | Stable identifier, used as React key and for bestStrategyId matching. |
label | ReactNode | Visible strategy name. |
bytesUntilPlay | number | Kilobytes the page pays before the user clicks play. |
ttfp | number | Time-to-first-paint of the video frame on click, in milliseconds. |
shortLabel | ReactNode | Optional short tag rendered above the label. |
tradeoff | ReactNode | Optional plain-prose tradeoff line. |
code | string | Optional code sample rendered in a <pre> block. |
Accessibility
- The wrapper is a
<section>withdata-cb-edu="video-strategy-board"andaria-labelledbywired to the title when one is provided. - The card grid is a
role="list"withrole="listitem"children, so assistive tech treats it as a list of strategies. - Each card carries
data-cb-strategy(the strategy id) anddata-cb-bestwhen it's the recommended pick — consumers can re-tone without touching the component. - Each metric
<dd>carriesdata-cb-verdictso the colour tone is mirrored as a data hook. - The "best pick" pill renders only as visible text. Screen-reader users learn about the recommendation through the surrounding prose; the badge itself is not announced as a live update.
- Under
prefers-reduced-motion, the card enter animation is skipped — cards render in place.
Credits
- Extracted from:
terminal-dreams(src/components/frontend-design/perf-other-assets/ui/VideoStrategyBoard.tsx). The original pulledfold,setFold, andbestVideooff anassets-perf-context, hard-coded a three-strategy roster (eager MP4 / poster-lazy / YouTube facade), bound the "best" highlight to a fold-position toggle, and reportedinitialKB/lcpDeltaMs/interactionDelayMsper card. This rewrite drops the context dependency, the fold toggle, and the LCP-delta column — consumers pass a flatstrategiesarray withbytesUntilPlayandttfpand pick the recommended id (if any) directly. The CSS-Module classnames are gone; the board consumes thecb-*token preset.