Cookbook Playground

A stepped interactive recipe stepper. Renders the active step as a card (title + description + a content slot), with Previous / Next buttons, a thin progress bar, a numbered step rail at the bottom, and an optional celebration slot that mounts when the user reaches the final step. Controlled or uncontrolled.

Slow-roast tomato confit

Tap Next to advance, or pick any numbered step to jump.

Progress1/4

Prep the tomatoes

Set up the tray so the oven step is hands-off.

Halve the tomatoes lengthwise. Arrange cut-side up on a wide oven tray and tuck whole peeled garlic cloves between them.
Customize
Regions

Installation

npx shadcn@latest add https://craftbits.dev/r/cookbook-playground.json

Usage

import { CookbookPlayground } from "@craft-bits/core";
 
<CookbookPlayground
  title="Slow-roast tomato confit"
  steps={[
    { id: "prep", title: "Prep the tomatoes", content: "Halve and tray." },
    { id: "season", title: "Season generously", content: "Oil, salt, thyme." },
    { id: "roast", title: "Slow roast at 140C", content: "2 to 2.5 hours." },
    { id: "finish", title: "Rest and serve", content: "On toast or in pasta." },
  ]}
/>

Controlled mode — drive activeStep from outside so the step index can feed an external timer, scrollytelling rig, or URL hash:

const [step, setStep] = useState(0);
 
<CookbookPlayground
  steps={STEPS}
  activeStep={step}
  onActiveStepChange={setStep}
  onComplete={(last) => console.log("done", last)}
/>

Drop a celebration onto the final step without re-wiring the controller:

<CookbookPlayground
  steps={STEPS}
  celebration={<CompletionCelebration totalTime={2700} />}
/>

Anatomy

  • Header. Optional title (rendered with the cb-label style) and an intro sub-line. Omit both for a chromeless playground.
  • Progress. A label + counter row above a thin animated bar that fills with the SPRINGS.snap token. Hide with hideProgress.
  • Step card. The active step's title, description, and content slot. A numbered pill on the left shows the 1-based index. The card crossfades when the step changes via <AnimatePresence> in mode wait.
  • Controls. A Previous / Next button row. The Next button changes its label to the doneLabel (Mark complete) when the user is on the final step. Hide with hideControls.
  • Step rail. A row of numbered dots at the bottom — active (filled accent), done (subdued accent), pending (muted). Tap any dot to jump. Hide with hideStepRail.
  • Celebration. An optional slot that mounts under the playground when the active step is the last one, animated in with the same snap spring.

Understanding the component

  1. Slot-driven steps, no project chrome. The original cookbook controller pulled in useStepNavigation, useIngredientTracking, useSound, a CookbookTimerProvider, a RecipeHeader, an IngredientPanel, a StepCard, and a CompletionCelebration. This rewrite keeps only the active step + controls + rail behaviour. Timers, ingredient highlighting, chef tips, and sound effects compose inside the content slot of each step — the playground itself is purely a stepper.
  2. Controlled + uncontrolled. Provide activeStep with onActiveStepChange to drive from outside (URL hash, parent scrollytelling, external simulator). Omit both and the playground manages internal state seeded by defaultActiveStep. Out-of-range values are clamped silently into the valid index range.
  3. Completion fires once per arrival. onComplete fires when the user lands on the final step. It re-arms once the user navigates back, so re-reaching the last step fires it again — useful for celebration replay. The Next button on the last step also calls onComplete so consumers can wire it to a custom finish action.
  4. Motion. The step card crossfades on id change via Motion's AnimatePresence with mode wait. The progress bar fills via a width animation on the SPRINGS.snap token. Every animation short-circuits under prefers-reduced-motion.

Props

PropTypeDefaultDescription
stepsCookbookPlaygroundStep[]requiredAll steps in display order.
activeStepnumberControlled active-step index.
defaultActiveStepnumber0Initial active step in uncontrolled mode.
onActiveStepChange(next: number) => voidFires on Next / Previous / rail click.
introReactNodeSub-headline under the playground title.
titleReactNodeOptional heading above the playground.
headingAs'h2' | 'h3' | 'h4''h3'Tag for the title element.
nextLabelReactNode'Next step'Label on the Next button.
previousLabelReactNode'Previous'Label on the Previous button.
doneLabelReactNode'Mark complete'Label on the Next button when the user is on the last step.
onComplete(lastIndex: number) => voidFires when the user reaches or finishes the final step.
hideStepRailbooleanfalseHide the numbered step rail.
hideProgressbooleanfalseHide the linear progress bar.
hideControlsbooleanfalseHide the Previous / Next button row.
celebrationReactNodeSlot mounted under the playground on the final step.
stepRailLabelstring'Recipe steps'ARIA label for the step rail.
classNamestringMerged onto the root via cn().

CookbookPlaygroundStep

FieldTypeDescription
idstringStable identifier — used as the React key and on data-cb-step-id.
titleReactNodeShort heading rendered above the step body.
descriptionReactNodeOptional one-line subtitle under the title.
contentReactNodeBody of the step — instructions, embedded widgets, code blocks.

Accessibility

  • The root is a <section> with data-cb-edu="cookbook-playground", data-cb-active-step, data-cb-total-steps, and data-cb-complete so consumers can extend per-state styling without re-deriving the props.
  • The progress bar carries role="progressbar" with aria-valuenow / aria-valuemin / aria-valuemax and an aria-label.
  • The step card pill (the numbered chip) is aria-hidden because the heading already conveys the step index. The numeric counter row uses an aria-label like Step 2 of 4 so the headline is conveyed without color.
  • The step rail is a <nav> with aria-label="Recipe steps". Each dot exposes aria-current="step" when active and aria-label like Go to step 3.
  • The Next button toggles its aria-label between Next step and Mark recipe complete so screen readers always know which action will fire.
  • The Previous button uses disabled plus an aria-label so it announces correctly when there is no earlier step.
  • All animations are opacity + transform only and short-circuit under prefers-reduced-motion.

Credits

  • Extracted from: terminal-dreams (src/components/cookbook/CookbookPlayground.tsx). The original was a controller that wired four cookbook hooks (useStepNavigation, useIngredientTracking, useSound, CookbookTimerProvider), required a CookbookRecipe shape with steps + ingredients + meta.totalTime, and rendered fixed <RecipeHeader> / <StepCard> / <IngredientPanel> / <CompletionCelebration> children inside a hard-wired <CookbookLayout>. craft-bits keeps only the step-sequencer behaviour — controlled + uncontrolled active step, deferred onComplete, slot-driven step card — so the same component drives recipes, tutorials, wizards, and explainers without dragging the timer / ingredient / sound stack along with it.