Prefetch Widget

A compact panel that visualizes hover-driven prefetching. Drop in a list of resources — route chunks, images, API payloads — and the widget renders one tile per entry. Hovering or focusing a tile starts a simulated prefetch whose duration scales with the resource's bytes. Clicking a tile "consumes" the resource and reports the percent that was already warm by the time of the click.

Preview

Hover-to-prefetch

Hover any tile to start its prefetch. Click to consume.

Legendidleprefetchingready
Customize
Behaviour
Bandwidth
60B/ms
Options

Installation

npx shadcn@latest add https://craftbits.dev/r/prefetch-widget.json

Usage

import { PrefetchWidget } from "@craft-bits/core";
 
<PrefetchWidget
  title="Hover-to-prefetch"
  resources={[
    { id: "products", label: "/products", bytes: 48_000 },
    { id: "cart", label: "/cart", bytes: 22_000 },
    { id: "about", label: "/about", bytes: 12_000 },
  ]}
  onSelect={(id, info) => {
    console.log(id, info.prefetchedPct);
  }}
/>

Flip enabled to false to model the "feature off" baseline — hover does nothing, every click is a cold fetch:

<PrefetchWidget enabled={false} resources={resources} />

Mark a resource as already warm — e.g. a service-worker hit or a previously-completed prefetch:

<PrefetchWidget
  resources={[
    { id: "shell", label: "/", bytes: 8_000, prefetched: true },
    { id: "next", label: "/next", bytes: 22_000 },
  ]}
/>

Anatomy

  • Header. Optional title (renders with the cb-label style) and description. Omit both for a chromeless panel.
  • Tile grid. Two columns on sm+, single column below. Each tile is a <button> by default; readOnly switches it to a <div> for a display-only mode.
  • Progress bar. A single row under the label/state chip. Animates via scaleX (transform-only — no width animation) so it stays on the compositor.
  • State chip. A small pill on the right of each tile — idle, prefetching, ready, or loaded. Tone follows the bar color.
  • Legend. A bottom strip explains the three primary states. Hide via hideLegend.

Understanding the component

  1. Hover intent starts a prefetch. onMouseEnter / onFocus start the simulated fetch; onMouseLeave / onBlur pause it. The progress survives until the user clicks (consumes) or hovers again to resume.
  2. Duration scales with bytes. bytes / bytesPerMs gives the total prefetch time in milliseconds, with a floor of 120ms so tiny resources still get a brief animation. bytesPerMs=60 ≈ a slow 4G connection; bump it up for fast pipes.
  3. prefetched seeds 100%. Resources flagged prefetched: true start at full warm — useful for modeling service-worker hits and prior cache state.
  4. enabled=false short-circuits hover. No animation, no progress — just bare tiles you can still click to model a cold fetch.
  5. onSelect reports the warm percent. The widget hands back { prefetchedPct, state } so consumers can teach "20 percent warm" vs "100 percent warm" tradeoffs in their own copy or a follow-up timeline.
  6. Motion. The progress bar is a scaleX motion.div driven by SPRINGS.damped (no overshoot). All animation is short-circuited under prefers-reduced-motion.

Props

PropTypeDefaultDescription
resourcesPrefetchWidgetResource[]requiredOrdered list of resources.
titleReactNodeOptional heading above the grid.
descriptionReactNodeOptional sub-headline under the title.
enabledbooleantrueFeature flag — when false, hover/focus do nothing.
bytesPerMsnumber60Effective bandwidth driving the prefetch duration.
formatBytes(bytes) => stringmagnitude-awareCustom byte formatter.
onSelect(id, info) => voidCalled on click with prefetchedPct and final state.
readOnlybooleanfalseRender tiles as non-interactive divs.
hideLegendbooleanfalseHide the legend strip under the grid.
headingAs'h2' | 'h3' | 'h4''h3'Tag for the title element.
classNamestringMerged onto the root via cn().

PrefetchWidgetResource

FieldTypeDescription
idstringStable identifier. Used as the React key and as the argument to onSelect.
labelReactNodeVisible name on the tile.
bytesnumberResource size — drives prefetch duration. Defaults to 0 (instant).
prefetchedbooleanStart at 100% warm.
hintReactNodeHelper text under the label. Falls back to the formatted byte count.

Accessibility

  • The wrapper is a <section> with data-cb-edu="prefetch-widget" and data-cb-enabled="true|false". Tiles carry data-cb-state="idle|prefetching|ready|consumed" so consumers can extend tone-specific styling without monkey-patching CSS.
  • Tiles are <button> elements by default with descriptive aria-label text — they're reachable by Tab and trigger the prefetch on focus (not just hover). readOnly switches them to non-interactive <div>s for display-only mode.
  • The progress bar is a role="progressbar" with aria-valuenow / aria-valuemin / aria-valuemax, so screen readers can announce the warm percent.
  • The grid is aria-live="polite" — state changes are announced without interrupting the user.
  • Animations are limited to the bar's scaleX and short-circuit under prefers-reduced-motion.

Credits

  • Extracted from: terminal-dreams (src/components/frontend-design/sdp-web-performance/ui/PrefetchWidget.tsx). The original was pinned to a Core Web Vitals lab — it pulled enabledOptimizations and activeProfile out of a PerfContext, hard-coded a list of /products / /cart / /about route links with click distributions, and rendered a Speculation Rules pipeline footer. This rewrite drops the context dependency and the project chrome — consumers pass their own { id, label, bytes, prefetched } rows so the widget covers any hover-prefetch use case (route chunks, hero images, API payloads, …), not just route prefetching.