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.

Preview

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 metadata
    best 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 />
Customize
Highlight
preload-metadata
Initial-cost thresholds
100KB
1000KB
TTFP thresholds
300ms
1500ms
Display

Installation

npx shadcn@latest add https://craftbits.dev/r/video-strategy-board.json

Usage

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 title and description. Both omittable for a chromeless board.
  • Strategy card. One per entry in strategies. Carries the strategy name, an optional shortLabel tag, a two-cell metric strip (bytes until play / TTFP on click), an optional tradeoff line, and an optional code sample.
  • Best-pick badge. When bestStrategyId matches the card's id, 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/over bad, orange in between.

Understanding the component

  1. Two numbers, that's it. Every strategy reports bytesUntilPlay (the cost the page pays before any click) and ttfp (the wait once the user clicks play). Both feed the metric strip and the per-cell verdict.
  2. Thresholds drive tone. bytesUntilPlayThresholds and ttfpThresholds each have a good and bad cutoff. Values at or under good rate good, values at or over bad rate bad, everything in between rates warn. The cell paints cb-success / cb-warning / cb-error accordingly.
  3. Best-pick highlight. bestStrategyId is a free-form opt-in — match it to any card's id and that card gets data-cb-best="true", an accent border, and a pill. Omit it entirely if you want a flat comparison with no recommendation.
  4. Grid scaling. The card grid is one column at the smallest breakpoint, two at sm, three at lg when there are at least three strategies, four at xl when there are at least four. Anything beyond four wraps onto a second row.
  5. Motion. Each card fades and rises in once on mount (spring snap). Under prefers-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

PropTypeDefaultDescription
strategiesVideoStrategyBoardStrategy[]requiredStrategies to compare. At least one.
titleReactNodeOptional heading above the comparison grid.
descriptionReactNodeOptional sub-headline under the title.
bestStrategyIdstringid 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.
hideCodebooleanfalseHide the code-sample block under each card.
hideTradeoffbooleanfalseHide the tradeoff prose line.
headingAs'h2' | 'h3' | 'h4''h3'Tag for the title element.
classNamestringMerged onto the root via cn().

VideoStrategyBoardStrategy

FieldTypeDescription
idstringStable identifier, used as React key and for bestStrategyId matching.
labelReactNodeVisible strategy name.
bytesUntilPlaynumberKilobytes the page pays before the user clicks play.
ttfpnumberTime-to-first-paint of the video frame on click, in milliseconds.
shortLabelReactNodeOptional short tag rendered above the label.
tradeoffReactNodeOptional plain-prose tradeoff line.
codestringOptional code sample rendered in a <pre> block.

Accessibility

  • The wrapper is a <section> with data-cb-edu="video-strategy-board" and aria-labelledby wired to the title when one is provided.
  • The card grid is a role="list" with role="listitem" children, so assistive tech treats it as a list of strategies.
  • Each card carries data-cb-strategy (the strategy id) and data-cb-best when it's the recommended pick — consumers can re-tone without touching the component.
  • Each metric <dd> carries data-cb-verdict so 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 pulled fold, setFold, and bestVideo off an assets-perf-context, hard-coded a three-strategy roster (eager MP4 / poster-lazy / YouTube facade), bound the "best" highlight to a fold-position toggle, and reported initialKB / lcpDeltaMs / interactionDelayMs per card. This rewrite drops the context dependency, the fold toggle, and the LCP-delta column — consumers pass a flat strategies array with bytesUntilPlay and ttfp and pick the recommended id (if any) directly. The CSS-Module classnames are gone; the board consumes the cb-* token preset.