Lists
Checklist
A list of things to tick off, such as a recipe's ingredients or a shopping list: each row a checkbox the whole row toggles, with an optional value and action. Checking a row fills its box, draws its tick and then a strike through its label, and softens its text; rows enter and leave as items changes, and "Clear checked" shows while something is checked.
In Spoonjoy
Spoonjoy, a recipe app, is built with Ouro HX (how).
See it live, no sign-in needed: A recipe · Test kitchen
In its source: app/components/recipe/IngredientList.tsx · app/routes/shopping-list.tsx
Install
Once per app, with Tailwind CSS v4 and an @/* path alias (Ouro HX is built on Base UI, so --base base; skip -t in an existing app):
npx shadcn@latest init --base base --preset nova --yes --no-monoreponpx shadcn@latest add ourostack/ouro-ui/ouro-base#v1.2.13Then the component. From v0.8.0 it installs only the helpers it imports and never touches your stylesheet (Installing):
npx shadcn@latest add ourostack/ouro-ui/checklist#v1.2.13Or, with the @ouro namespace set up:
npx shadcn@latest add @ouro/ouro-base @ouro/checklistIts tests, stories and Playwright spec are optional. They need Vitest 4.1.11 or later, and from v0.8.0 never change a version your app has (Testing):
npx shadcn@latest add ourostack/ouro-ui/checklist#v1.2.13 ourostack/ouro-ui/checklist-tests#v1.2.13import { Checklist } from "@/components/ouro/checklist/checklist";Paths use shadcn's default @/ alias; the CLI rewrites them to yours.
Example
<Checklist
label="Ingredients"
items={[{ id: "oats", label: "rolled oats", value: "1 cup", checked: checked.has("oats") }]}
onCheckedChange={(id, isChecked) => toggle(id, isChecked)}
onClearChecked={clearAll}
/>Use instead when
- Use
CheckboxGroupinstead when the choices submit with a form; useChecklistfor progress a person ticks off.
Props
Type: ChecklistProps.
className- Type:
string - Layout only: margins, width, grid placement (
mt-4,col-span-2). Appearance classes warn in development; style checklists in your theme. clearLabel- Type:
string - Default:
"Clear checked" - The clear button's text. Translate it here.
description- Type:
ReactNode - Supporting text under the label, read after it through
aria-describedby. empty- Type:
ReactNode - What shows when there are no rows: text, or an
Empty. hideLabel- Type:
boolean - Hides the label visually; screen readers still read it as the group's name.
items(required)- Type:
readonly ChecklistItemData[] - The rows, in order. Adding one brings it in; removing one lets it leave before it goes, and an id added back while its row leaves returns that row in place.
label(required)- Type:
string - What the list is ("Ingredients"), shown above the rows and read as the group's name. Required: a group needs a name. To hide it visually, keep it and add
hideLabel. onCheckedChange(required)- Type:
(id: string, checked: boolean) => void - Called with a row's id and its new state when it is checked or unchecked. Required: a checklist without it never changes.
onClearChecked- Type:
(() => void) - Shows a "Clear checked" button while something is checked, and is called when it is pressed.
ref- Type:
Ref<HTMLDivElement> - The group element, a
<div role="group">.
Don't use
These are not props. Passing one fails the type check with the fix in the error, and in development a console message names the fix.
isChecked: Not a prop. Each item says whether it is checked:items={[{ id, label, checked: true }]}.legend: Not a prop. A checklist is named by itslabel:<Checklist label="Ingredients">.onChange: Not a prop. Ouro UI usesonCheckedChange, which gets the row's id and its new state.onCheck: Not a prop. Ouro UI usesonCheckedChange, which gets the row's id and its new state.onToggle: Not a prop. Ouro UI usesonCheckedChange, which gets the row's id and its new state.
className is for layout only
Pass margins, width and grid placement (mt-4, w-full, col-span-2).
Appearance comes from props, not classes. In development, a class that sets appearance logs one warning per component instance, naming the prop to use instead. A class sets appearance when, after its variants (hover:, sm:) and any ! are removed, it starts with bg-, text-, border-, rounded-, shadow-, font-, ring-, from-, via-, to-, fill- or stroke-, or is border, rounded, shadow or ring, or has a dark: variant. Text wrapping, overflow and table layout classes (text-wrap, text-ellipsis, border-collapse) are layout and do not warn.
Alignment classes (text-center) warn too: set alignment on a parent element.
Parts
When these props cannot express your layout, build it from the parts in @/components/ouro/checklist/checklist-parts. The nesting they need follows.
import {
ChecklistItem,
ChecklistRoot,
} from "@/components/ouro/checklist/checklist-parts";Required nesting
<ChecklistRoot label="Steps">
<ChecklistItem label="Preheat the oven" checked={done.oven} onCheckedChange={(checked) => mark("oven", checked)} />
</ChecklistRoot>Each part checks that it sits inside <ChecklistRoot>: outside it, a part throws in development with a message that shows this JSX shape.checklist-nesting
ChecklistItem- What it is: One row of a
ChecklistRoot. The whole row but itsactiontoggles it: a label holding the box, the text and the value, with the action beside it. Use theChecklistcomponent instead unless itsitemscannot express your rows. ChecklistRoot- What it is: The group a checklist built from parts renders its rows in, named by
label. Use theChecklistcomponent instead unless itsitemscannot express your rows; a row removed from a ChecklistRoot goes at once, whileChecklistlets it leave.
Types
ChecklistItemData
One row of a Checklist.
interface ChecklistItemData {
/** Identifies the row: `onCheckedChange` gets it, and a row keeps its place and motion by it. */
id: string;
/** What to tick off, read as the row's checkbox name. */
label: string;
/** Supporting text under the label: a note, a substitution. */
description?: ReactNode | undefined;
/** Trailing text at the end of the row, such as an amount (`1 cup`) or a time; screen readers hear it after the label. */
value?: ReactNode | undefined;
/** Whether the row is checked. */
checked: boolean;
/** Disables the row's box and fades it. */
disabled?: boolean | undefined;
/** A control of the row's own beside it (a button, a menu), its own tab stop; clicking it does not toggle the row. */
action?: ReactNode | undefined;
}