Grid disclosure
A one-open-at-a-time list whose panels open to their own height through grid rows, with no measuring.
Behaviour
- Each panel animates its grid row between 0fr and 1fr, so it opens to whatever its content measures without anyone measuring it.
- Opening a row closes the one that was open; clicking the open row closes it.
- Rows are real buttons with aria-expanded and aria-controls; closed panels are inert, so Tab never lands inside a hidden answer. Titles align to the reading start, so RTL works.
- Reduced motion: panels only fade and the caret flips without turning.
Props
| Prop | Type | Description |
|---|---|---|
| items | { title: ReactNode; content: ReactNode }[] | The rows, in order. Only one is open at a time. A row with an empty title is skipped; indices still count it. |
| value | number | null | Controlled open row (index), or null for all closed. Leave it out and the list keeps its own state. |
| defaultValue | number | nulldefault 0 | Starting open row when uncontrolled; null starts all closed. |
| onValueChange | (value: number | null) => void | Called with the newly open row, or null when the open one closes. |
| icon | ReactNode | Replaces the built-in caret. It still turns half a circle when its row opens. |
| className | string | Merged onto the <ul> with cn(). |
Installation
npx shadcn@latest add https://purrfectui.com/r/grid-disclosure.jsonHow to use
import { GridDisclosure } from "@/components/ui/grid-disclosure";
<GridDisclosure
items={[
{ title: "What exactly am I approving?", content: "The precise bytes of the outbound comment." },
{ title: "How is scope enforced?", content: "At the capability boundary." },
]}
/>Source code
Loading…
Credits
Ported from bountydesk.vaibhav.quest.
License
MIT. Copy it into your project and change it as you like.