Ingredient Panel

Checklist-style panel for listing ingredients, inputs, or parameters in a recipe-like flow. Each row has a label, an optional amount in monospace, an optional description, and a check state. Flip on groupable to send completed rows to a bottom "Done" group with a smooth layout animation.

Different from StateInspector (a read-only debugger watch list): the Ingredient Panel is interactive — the user marks items as ready.

Ingredients
Customize
Layout

Installation

npx shadcn@latest add https://craftbits.dev/r/ingredient-panel.json

Usage

import { IngredientPanel } from "@craft-bits/core";
 
<IngredientPanel
  title="Ingredients"
  ingredients={[
    { id: "flour", label: "Flour", amount: "250g" },
    { id: "sugar", label: "Sugar", amount: "100g" },
    { id: "eggs", label: "Eggs", amount: "2" },
  ]}
/>

Understanding the component

  1. Native semantics. Each row is a real <label> wrapping a real visually-hidden <input type="checkbox">. Keyboard users get Tab + Space; screen readers announce "checkbox, label, checked/unchecked". The label's focus-within:outline delegates the focus ring from the hidden input to the visible row.
  2. Controlled + uncontrolled. Pass checkedIds for full control, or defaultCheckedIds for an initial set the component manages internally. onCheckedChange fires with the new id array in both modes.
  3. Strike-through plus dim. Checked rows render with line-through on the label and a 60% opacity drop on the row. Amount and description text shift to a softer tone so the checked row visually recedes.
  4. Groupable mode. When groupable is on, completed rows partition into a bottom "Done" section. Each row is a motion.li with a per-id layoutId, so the move animates via SPRINGS.smooth rather than disappearing and reappearing.
  5. Compact mode. Hides each row's description and tightens vertical padding. Useful in a narrow sidebar.
  6. Title is ReactNode. The native HTML title attribute is a string, so the component omits it from the spread and re-introduces a ReactNode version for rich titles (icons, formatted labels).

Props

PropTypeDefaultDescription
ingredientsIngredientPanelItem[]requiredRows to render. Each has id, label, optional amount and description.
checkedIdsstring[]Controlled list of checked ids. Pair with onCheckedChange.
defaultCheckedIdsstring[][]Uncontrolled initial checked ids.
onCheckedChange(ids: string[]) => voidFires when the checked set changes.
titleReactNodeOptional small-caps title above the list.
groupablebooleanfalseMove completed rows to a bottom "Done" group with a layout animation.
compactbooleanfalseHide descriptions and tighten padding.
classNamestringMerged onto the root via cn().

Accessibility

  • Root element uses role="group" and aria-label="Ingredients" so screen readers announce the collection.
  • Every row is a native <label> wrapping a real <input type="checkbox">. Tab reaches it, Space toggles it.
  • The visible row gets a focus ring via focus-within:outline so keyboard users see where the input is even though the input itself is visually hidden.
  • Strike-through is paired with an opacity drop so colour-blind users still perceive the state change.
  • Layout animation respects prefers-reduced-motion via motion's built-in handling on layout transitions.

Credits

  • Extracted from: terminal-dreams (src/components/cookbook/IngredientPanel.tsx). The library version drops the source's currentStepIngredientRefs / preparedIngredients lesson-specific props, generalises the row to { id, label, amount?, description? }, adds a groupable mode with a layoutId-driven layout transition, and swaps transition-all for explicit colour transitions to satisfy the lint gate.