Unfold List
A disclosure list whose panels turn down onto the page from their top edge.
Installation
npx shadcn@latest add @loomui/unfold-listUsage
import { UnfoldItem, UnfoldList } from "@/components/ui/unfold-list"<UnfoldList defaultValue="own">
<UnfoldItem value="own" title="Do I own the code?">
Every component is copied into your project as a plain file.
</UnfoldItem>
<UnfoldItem value="deps" title="What does it install?">
The file, and the tokens it needs in your stylesheet.
</UnfoldItem>
</UnfoldList>A panel opens to whatever height its content happens to be. The row it sits in is a grid track animated from 0fr to 1fr. Nothing has to measure the content first. There is no fixed height to keep in
sync when the content changes, the window resizes, or a font finally loads.
On top of that, the panel turns down onto the page from its top edge rather than sliding up from underneath. That is the difference between paper unfolding and a drawer opening. It is why the content tilts very slightly away from you until the fold finishes.
One at a time, or many
<UnfoldList type="multiple" defaultValue={["own", "deps"]}>single is the default: opening a panel closes the one that was open, which
suits an FAQ where the answers are alternatives. multiple lets a reader open
as many as they want, which suits documentation where they are steps.
In single mode, collapsible decides whether the open panel can be closed
again by clicking its own row. Leave it on unless something below the list
depends on a panel always being open.
Controlled
const [open, setOpen] = React.useState<string[]>(["own"])
<UnfoldList value={open} onValueChange={setOpen}>Pass value and the list stops holding its own state. onValueChange is
always called with an array, in both modes, so the handler does not change
shape when the type does.
Props
UnfoldList
| Prop | Type | Default | Description |
|---|---|---|---|
type | "single" | "multiple" | "single" | One panel open at a time, or many. |
defaultValue | string | string[] | none | Panels open before anyone touches it. |
value | string | string[] | none | Open panels, if you hold the state. |
onValueChange | (value: string[]) => void | none | Called with the open panels. |
collapsible | boolean | true | In single mode, allow closing it again. |
duration | number | 320 | Milliseconds one panel takes to unfold. |
UnfoldItem
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | none | Identifies the panel. Unique in the list. |
title | ReactNode | none | The row, which is the button that opens it. |
className | string | none | Merged onto the item. |
Any other <div> prop is forwarded.
Accessibility
Each row is a real button carrying aria-expanded and pointing at its panel
with aria-controls; the panel is a labelled region pointing back at its row.
A closed panel is marked inert. That takes it out of the tab order and off
the accessibility tree while it can still animate closed, which hidden would
not allow. Under prefers-reduced-motion panels change state with no fold at
all.