Progress Narrator

A contextual narrator for progress journeys. You hand it an ordered list of milestones and a numeric current value; it picks the highest-threshold milestone that has been reached and announces it via a small fade-and-slide swap. The narrator stays silent until the first checkpoint is crossed.

Preview

Fresh start — your first checkpoint is up ahead.

checkpoint 0 / 4
Customize
Options

Installation

npx shadcn@latest add https://craftbits.dev/r/progress-narrator.json

Usage

import { ProgressNarrator } from "@craft-bits/core";
 
const MILESTONES = [
  { id: "warmup", threshold: 1, text: "First checkpoint cleared." },
  { id: "midway", threshold: 2, text: "Halfway there — the pattern is locking in." },
  { id: "home",   threshold: 3, text: "Last lap." },
];
 
<ProgressNarrator milestones={MILESTONES} current={2} />

Drive current from any progress source — completed steps, scroll depth, score:

const completed = useCompletedSteps();
 
<ProgressNarrator milestones={MILESTONES} current={completed.length} />

Anatomy

  • Live region — the outer wrapper is role="region" with aria-live="polite". Each milestone swap is announced once without interrupting in-flight speech.
  • Milestone paragraph — the single active milestone, painted with the cb-accent left edge and a faint accent-muted backdrop. Keyed by id inside AnimatePresence so swaps fade-and-slide.
  • Silent idle state — when no milestone has been reached yet, the region renders empty so the narrator can mount eagerly at the top of a page.

Props

PropTypeDefaultDescription
milestonesReadonlyArray<ProgressNarratorMilestone>List of milestones, each with a stable id, a numeric threshold, and a text ReactNode. Order does not matter.
currentnumberCurrent progress value. Crossing a higher threshold swaps the announcement.
regionAriaLabelstring"Progress narration"Accessible name for the live region.
classNamestringMerged onto the root via cn().

Accessibility

  • The root is a role="region" with aria-live="polite", so screen readers announce each milestone swap without interrupting in-flight speech.
  • Override regionAriaLabel when the surrounding domain language calls for it (e.g. "Wave context", "Lesson progress").
  • Animation respects prefers-reduced-motion — with the system flag set, the slide-in collapses to a pure opacity fade via the same SPRINGS.smooth transition.

Credits

  • Extracted from: AlgoFlashcards (src/lessons/primitives/chrome/ProgressNarrator.tsx). Replaced the project-specific useProblemProgress localStorage subscription, RichText, and the useId-driven collapsible chrome with a generic numeric-milestone API. The original picked between five hand-named states (first, mid-fresh, all-review, first-review, mid-review) keyed off a wave-slug array; the extracted version generalises that to threshold-based milestones so the same primitive can narrate scroll depth, score, completed-step count, or any other numeric progress signal.