Lists

Collapsible

One section that opens and closes under its title, such as nutrition facts under a recipe. The content grows to its measured height with CSS, the closed content is in the server HTML and found by the page search of browsers that support hidden="until-found", and the title is a 44px row on touch screens.

In Spoonjoy

Spoonjoy, a recipe app, is built with Ouro HX (how).

See it live, no sign-in needed: API playground

In its source: app/routes/cookbooks.$id.tsx · app/routes/developers.playground.tsx · app/routes/oauth.authorize.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/collapsible#v1.2.13

Or, with the @ouro namespace set up:

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

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/collapsible#v1.2.13 ourostack/ouro-ui/collapsible-tests#v1.2.13
import { Collapsible } from "@/components/ouro/collapsible/collapsible";

Paths use shadcn's default @/ alias; the CLI rewrites them to yours.

Example

<Collapsible title="Nutrition facts">
  <NutritionTable />
</Collapsible>

Use instead when

  • Use Accordion instead for a stack of such sections.
  • Use ShowMore instead to cut a long list or text short behind a "Show more" button.
  • Use Dialog or Sheet instead when the content is a task of its own.

Props

Type: CollapsibleProps.

children (required)
Type: ReactNode
What shows while it is open.
className
Type: string
Layout only: margins, width, grid placement (mt-4). Appearance classes warn in development.
defaultOpen
Type: boolean
Opens it first, when the component keeps its own state.
disabled
Type: boolean
Keeps it from opening or closing.
onOpenChange
Type: ((open: boolean) => void)
Called with the new state when the person opens or closes it.
open
Type: boolean
Whether it is open, when you control it. Pair it with onOpenChange.
ref
Type: Ref<HTMLDivElement>
Reaches the rendered element.
title (required)
Type: ReactNode
The title, in view whether open or closed. It is the button that opens and closes the content.

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.

  • isOpen: Not a prop. Ouro UI uses open: <Collapsible open>.
  • label: Not a prop. Ouro UI uses title: <Collapsible title="Nutrition facts">.
  • onChange: Not a prop. Ouro UI uses onOpenChange: <Collapsible onOpenChange={(open) => …}>.
  • trigger: Not a prop. The button is drawn for you from title: <Collapsible title="Nutrition facts">.

className is for layout only

Pass margins, width and grid placement (mt-4, w-full, col-span-2). Your classes always win over Ouro UI's: Collapsible merges className last with cn (tailwind-merge), so a class that conflicts with its own replaces it.layout-classes

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.

Claim sources