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.jsonUsage
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 thecb-labelstyle) anddescription. Omit both for a chromeless panel. - Tile grid. Two columns on
sm+, single column below. Each tile is a<button>by default;readOnlyswitches 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, orloaded. Tone follows the bar color. - Legend. A bottom strip explains the three primary states. Hide via
hideLegend.
Understanding the component
- Hover intent starts a prefetch.
onMouseEnter/onFocusstart the simulated fetch;onMouseLeave/onBlurpause it. The progress survives until the user clicks (consumes) or hovers again to resume. - Duration scales with bytes.
bytes / bytesPerMsgives 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. prefetchedseeds 100%. Resources flaggedprefetched: truestart at full warm — useful for modeling service-worker hits and prior cache state.enabled=falseshort-circuits hover. No animation, no progress — just bare tiles you can still click to model a cold fetch.onSelectreports 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.- Motion. The progress bar is a
scaleXmotion.divdriven bySPRINGS.damped(no overshoot). All animation is short-circuited underprefers-reduced-motion.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
resources | PrefetchWidgetResource[] | required | Ordered list of resources. |
title | ReactNode | — | Optional heading above the grid. |
description | ReactNode | — | Optional sub-headline under the title. |
enabled | boolean | true | Feature flag — when false, hover/focus do nothing. |
bytesPerMs | number | 60 | Effective bandwidth driving the prefetch duration. |
formatBytes | (bytes) => string | magnitude-aware | Custom byte formatter. |
onSelect | (id, info) => void | — | Called on click with prefetchedPct and final state. |
readOnly | boolean | false | Render tiles as non-interactive divs. |
hideLegend | boolean | false | Hide the legend strip under the grid. |
headingAs | 'h2' | 'h3' | 'h4' | 'h3' | Tag for the title element. |
className | string | — | Merged onto the root via cn(). |
PrefetchWidgetResource
| Field | Type | Description |
|---|---|---|
id | string | Stable identifier. Used as the React key and as the argument to onSelect. |
label | ReactNode | Visible name on the tile. |
bytes | number | Resource size — drives prefetch duration. Defaults to 0 (instant). |
prefetched | boolean | Start at 100% warm. |
hint | ReactNode | Helper text under the label. Falls back to the formatted byte count. |
Accessibility
- The wrapper is a
<section>withdata-cb-edu="prefetch-widget"anddata-cb-enabled="true|false". Tiles carrydata-cb-state="idle|prefetching|ready|consumed"so consumers can extend tone-specific styling without monkey-patching CSS. - Tiles are
<button>elements by default with descriptivearia-labeltext — they're reachable by Tab and trigger the prefetch onfocus(not just hover).readOnlyswitches them to non-interactive<div>s for display-only mode. - The progress bar is a
role="progressbar"witharia-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
scaleXand short-circuit underprefers-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 pulledenabledOptimizationsandactiveProfileout of aPerfContext, hard-coded a list of/products//cart//aboutroute 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.