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.

Ingredients

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-monorepo
npx shadcn@latest add ourostack/ouro-ui/ouro-base#v1.2.13

Then 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.13

Or, with the @ouro namespace set up:

npx shadcn@latest add @ouro/ouro-base @ouro/checklist

Its 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.13
import { 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 CheckboxGroup instead when the choices submit with a form; use Checklist for 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 its label: <Checklist label="Ingredients">.
  • onChange: Not a prop. Ouro UI uses onCheckedChange, which gets the row's id and its new state.
  • onCheck: Not a prop. Ouro UI uses onCheckedChange, which gets the row's id and its new state.
  • onToggle: Not a prop. Ouro UI uses onCheckedChange, 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 its action toggles it: a label holding the box, the text and the value, with the action beside it. Use the Checklist component instead unless its items cannot express your rows.
ChecklistRoot
What it is: The group a checklist built from parts renders its rows in, named by label. Use the Checklist component instead unless its items cannot express your rows; a row removed from a ChecklistRoot goes at once, while Checklist lets 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;
}