Font Widget

A teaching widget for the four CSS font-display strategies — block, swap, fallback, optional. The widget renders a sample string and walks it through the FOIT, FOUT, and swap phases each strategy produces, against a simulated network load delay you can tune live. A metric strip underneath reports the resulting FOIT duration, swap behaviour, and CLS impact.

Preview

Font loading

Pick a strategy and watch the text walk through the phases.

The quick brown fox jumps over the lazy dog.

font-display

Fallback visible immediately, swaps when the font loads. Zero FOIT, but a layout shift on swap.

  • FOITNone
  • CLS impactAlways shifts
  • Load delay1500ms
Customize
Simulation
swap
1500ms
Display

Installation

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

Usage

import { FontWidget } from "@craft-bits/core";
 
<FontWidget
  title="Font loading"
  sampleText="The quick brown fox jumps over the lazy dog."
  loadDelayMs={1500}
/>

Controlled mode — drive the strategy from outside, e.g. from a lesson stepper:

const [strategy, setStrategy] = useState<FontWidgetStrategy>("swap");
 
<FontWidget strategy={strategy} onStrategyChange={setStrategy} />

Anatomy

  • Header. Optional title and description. Both omittable for a chromeless variant.
  • Preview pane. Renders the sample string in either the fallback font or the (notionally) loaded web font, depending on the current phase. The pane is aria-live="polite" so screen readers announce the phase change.
  • Strategy picker. A <fieldset> wrapping a radiogroup of four chips, one per font-display value. Selecting one resets the simulation.
  • Metric strip. Three cells — FOIT, CLS impact, and the configured load delay — toned by the strategy's verdict.

Understanding the component

  1. Phase model. The widget walks through four phases: invisible (FOIT), fallback (FOUT showing the system font), loaded (the web font swapped in), and fallback-final (the system font kept for the rest of the view). Each strategy maps the simulated tick count to one of these phases.
  2. Simulated load delay. loadDelayMs is how long the widget pretends the network takes to deliver the font. The picker resets a requestAnimationFrame loop and walks an internal tickMs from 0 to loadDelayMs + 200, then settles.
  3. Reduced motion. Under prefers-reduced-motion, the loop is skipped entirely — the widget snaps straight to the steady-state phase for the chosen strategy.
  4. Verdict. Each strategy carries a static verdict (good / neutral / bad) used to tone the metric row. block is bad because of FOIT; swap is neutral because of CLS; fallback and optional are good.

Variants

// Slow connection — load delay past 3s pushes fallback into fallback-final.
<FontWidget loadDelayMs={3200} defaultStrategy="fallback" />
 
// Embedded inline — hide the metric strip when used inside prose.
<FontWidget hideMetrics />

Props

PropTypeDefaultDescription
titleReactNode'Font loading'Optional heading above the preview pane.
descriptionReactNodeOptional sub-headline under the title.
strategyFontWidgetStrategyControlled active strategy.
defaultStrategyFontWidgetStrategy'swap'Initial strategy in uncontrolled mode.
onStrategyChange(s: FontWidgetStrategy) => voidFires when the user picks a new strategy.
sampleTextstringpangramSample string rendered in the preview pane.
loadDelayMsnumber1500Simulated network load delay in milliseconds.
hideMetricsbooleanfalseHide the bottom metric strip.
headingAs'h2' | 'h3' | 'h4''h3'Tag for the title element.
classNamestringMerged onto the root via cn().

FontWidgetStrategy is the union 'block' | 'swap' | 'fallback' | 'optional'.

Accessibility

  • The wrapper is a <section> with data-cb-edu="font-widget", plus data-strategy and data-phase so consumers can extend tone-specific styling without monkey-patching CSS.
  • The preview pane is aria-live="polite" and aria-atomic="true" — phase changes announce as one sentence rather than piecemeal updates.
  • The strategy picker is a real <fieldset> + role="radiogroup" of role="radio" buttons, each with aria-checked. Buttons use native button semantics for keyboard activation; the radios use data-state for styling.
  • A visually-hidden sentence inside the preview pane ("Web font rendered." vs "Fallback font rendered.") conveys the visible/invisible flip without depending on font weight.
  • Under prefers-reduced-motion, the simulation loop is skipped — the widget snaps to the steady-state phase without animating.

Credits

  • Extracted from: terminal-dreams (src/components/frontend-design/sdp-web-performance/ui/FontWidget.tsx). The original pulled enabledOptimizations, activeProfile, and optParams off a PerfContext and rendered a fixed before/after pipeline timeline. This rewrite drops the context dependency and the binary on/off framing — consumers pick any of the four strategies and tune the simulated load delay directly. The CSS-Module classnames are gone; the widget now consumes our token preset.