Step Caption

The single-line caption that sits beneath a visualization and tells the reader what just happened (or what is about to). When the step changes, the previous caption cross-fades out by 6px and the next caption fades in from the same offset under SPRINGS.smooth.

Reach for it when an animated visual needs a narration line that turns over on every step — algorithm traces, walkthroughs, multi-phase explainers. Sibling to PhaseTransition, which morphs an entire body region; StepCaption morphs only a single narration line, without chrome.

Step 2 / 4

Compare each element to the current max — replace when it is larger.

Customize
Step
compare
Chrome

Installation

npx shadcn@latest add https://craftbits.dev/r/step-caption.json

Usage

import { StepCaption } from "@craft-bits/core";
 
<StepCaption
  stepKey={step.id}
  kicker="Step 2 / 4"
  text="Compare each element to the current max — replace when it is larger."
/>

Drop the kicker for a bare narration line:

<StepCaption stepKey={step.id} text={step.caption} />

Mark the takeaway caption with the accent color:

<StepCaption stepKey="done" text="The array is unchanged." accent />

Understanding the component

  1. Keyed cross-fade. The caption is wrapped in <AnimatePresence mode="popLayout" initial={false}> and keyed by stepKey (defaulting to the stringified text). When the key changes, the previous caption fades + slides out by 6px while the next fades in from the same offset — both in parallel under SPRINGS.smooth.
  2. initial={false}. The first render does not animate; only subsequent step changes do. Skips the first-render fade so the page does not flicker on load.
  3. mode="popLayout". Old and new captions coexist mid-transition without pushing siblings around — a tall caption cross-fading with a short one does not cause sibling content to twitch.
  4. Two regions, one rhythm. The optional kicker and the body share the same key and transition, so the row breathes together rather than the kicker arriving on a different beat.
  5. Reduced motion. When prefers-reduced-motion: reduce is set, the y-offset collapses to 0 and the transition duration drops to 0 — the swap becomes instant.
  6. Aria-live polite. The body region carries role="status" and aria-live="polite" so screen readers re-announce the new caption when the step advances, without interrupting the user.

Props

PropTypeDefaultDescription
textReactNoderequiredCaption body for the current step.
kickerReactNodeOptional small prefix label above the body. Omit to render only the body.
stepKeystring | numberderived from textStable AnimatePresence key. Override when text changes mid-step but the cross-fade should not.
accentbooleanfalseRender the body in the accent foreground color instead of the default muted body color.
classNamestringMerged onto the root <div> via cn().

Accessibility

  • The caption is announced via role="status" + aria-live="polite" so screen readers re-read the new content on swap without interrupting.
  • Animation is transform + opacity only — never width / height / top / left.
  • Exiting captions receive pointer-events: none so a mid-transition pointer cannot interact with content that is on its way out.
  • prefers-reduced-motion: reduce short-circuits both the y-offset and the spring — the swap becomes instant.
  • The default text-cb-fg-muted body color and the text-cb-accent accent variant meet WCAG AA contrast against the default surface; verify with custom palettes.

Credits

  • Extracted from: algoflashcards (src/lessons/primitives/chrome/StepCaption.tsx). The source coupled the caption to RichText narration and a bag-derived stepKey. craft-bits' version drops that plumbing, surfaces an optional kicker, and adds an accent flag so the same primitive can also serve as the takeaway-line variant.