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.jsonUsage
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
- 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'sfocus-within:outlinedelegates the focus ring from the hidden input to the visible row. - Controlled + uncontrolled. Pass
checkedIdsfor full control, ordefaultCheckedIdsfor an initial set the component manages internally.onCheckedChangefires with the new id array in both modes. - Strike-through plus dim. Checked rows render with
line-throughon 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. - Groupable mode. When
groupableis on, completed rows partition into a bottom "Done" section. Each row is amotion.liwith a per-idlayoutId, so the move animates viaSPRINGS.smoothrather than disappearing and reappearing. - Compact mode. Hides each row's
descriptionand tightens vertical padding. Useful in a narrow sidebar. - Title is
ReactNode. The native HTMLtitleattribute is a string, so the component omits it from the spread and re-introduces aReactNodeversion for rich titles (icons, formatted labels).
Props
| Prop | Type | Default | Description |
|---|---|---|---|
ingredients | IngredientPanelItem[] | required | Rows to render. Each has id, label, optional amount and description. |
checkedIds | string[] | — | Controlled list of checked ids. Pair with onCheckedChange. |
defaultCheckedIds | string[] | [] | Uncontrolled initial checked ids. |
onCheckedChange | (ids: string[]) => void | — | Fires when the checked set changes. |
title | ReactNode | — | Optional small-caps title above the list. |
groupable | boolean | false | Move completed rows to a bottom "Done" group with a layout animation. |
compact | boolean | false | Hide descriptions and tighten padding. |
className | string | — | Merged onto the root via cn(). |
Accessibility
- Root element uses
role="group"andaria-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:outlineso 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-motionviamotion's built-in handling onlayouttransitions.
Credits
- Extracted from:
terminal-dreams(src/components/cookbook/IngredientPanel.tsx). The library version drops the source'scurrentStepIngredientRefs/preparedIngredientslesson-specific props, generalises the row to{ id, label, amount?, description? }, adds agroupablemode with alayoutId-driven layout transition, and swapstransition-allfor explicit colour transitions to satisfy the lint gate.