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

PropTypeDescription
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.
valuenumber | nullControlled open row (index), or null for all closed. Leave it out and the list keeps its own state.
defaultValuenumber | nulldefault 0Starting open row when uncontrolled; null starts all closed.
onValueChange(value: number | null) => voidCalled with the newly open row, or null when the open one closes.
iconReactNodeReplaces the built-in caret. It still turns half a circle when its row opens.
classNamestringMerged onto the <ul> with cn().

Installation

npx shadcn@latest add https://purrfectui.com/r/grid-disclosure.json

How 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.

0