Fold panel
FreeAn accordion whose sections are hinged to their headers. Opening turns a section down from edge-on to flat while its height opens, so you can see which header it belongs to and where it goes when it closes. It is the standard accordion underneath: header buttons, aria-expanded, regions named by their header.
pnpm dlx shadcn@latest add @caliper/fold-panel- Address
- 2 Rosewood Lane, 1780 Halden
- Method
- Tracked, 2 days
- Cost
- $6.00
- Card
- Visa ending 4821
- Billing name
- Mira Solheim
- Charge date
- On dispatch
- Address
- Same as shipping
- VAT number
- NO 912 345 678
- For the courier
- Leave with the neighbour at number 4 if nobody answers
When to use #
Use it for
- Checkout, settings and order summaries where each section has a one-line summary worth showing while closed.
- Anywhere an accordion should feel attached to its header instead of sliding in from nowhere.
Skip it when
- Long article text; a fold per paragraph is a lot of motion.
- Navigation; use the sidebar or tabs.
Decisions #
Why it is built this way, so you know what to keep when you change your copy.
Behaviour
- Depth carries attachment
- The hinge is the header's bottom edge. The section is the only thing that turns, so the text is flat the moment it is readable.
- One open or several is a prop
- type is single or multiple, and value is always the list of open ids, so switching between them never changes how you read state.
Usage
- Closed means inert
- A closed section is inert, so Tab never lands in it and screen readers skip it. Focus stays on the header you pressed.
Installation #
pnpm dlx shadcn@latest add @caliper/fold-panel
Adds its 4 files to components/ui/, each with its own stylesheet, and installs their packages. No account needed. The first part also adds Caliper UI’s tokens; import them once as in Installation.
Usage #
This is the code behind the preview above.
import { useState } from "react" import { FoldPanel, type FoldSection } from "@/components/ui/fold-panel" import { SegmentedControl } from "@/components/ui/segmented-control" const dl = (rows: readonly (readonly [string, string])[]) => ( <dl> {rows.map(([term, value]) => ( <div key={term}> <dt>{term}</dt> <dd>{value}</dd> </div> ))} </dl> ); const order: FoldSection[] = [ { id: "shipping", title: "Shipping", summary: "2 Rosewood Lane, Halden", content: dl([["Address", "2 Rosewood Lane, 1780 Halden"], ["Method", "Tracked, 2 days"], ["Cost", "$6.00"]]) }, { id: "payment", title: "Payment", summary: "Visa ending 4821", content: dl([["Card", "Visa ending 4821"], ["Billing name", "Mira Solheim"], ["Charge date", "On dispatch"]]) }, { id: "billing", title: "Billing address", summary: "Same as shipping", content: dl([["Address", "Same as shipping"], ["VAT number", "NO 912 345 678"]]) }, { id: "notes", title: "Notes", summary: "Leave with neighbour", content: dl([["For the courier", "Leave with the neighbour at number 4 if nobody answers"]]) }, ]; export function FoldPanelDemo() { const [type, setType] = useState<"single" | "multiple">("single"); const [open, setOpen] = useState<string[]>(["shipping"]); return ( <div className="grid w-full max-w-md justify-items-center gap-3"> <SegmentedControl label="How many open" value={type} onValueChange={(v) => setType(v === "multiple" ? "multiple" : "single")} options={[{ value: "single", label: "One open" }, { value: "multiple", label: "Several open" }]} /> <div className="w-full"> <FoldPanel sections={order} type={type} value={open} onValueChange={setOpen} /> </div> </div> ); }
API reference #
FoldPanel
component
Accessibility #
It is built on native elements and keeps aria-expanded, aria-controls, aria-hidden and aria-labelledby in step with state.
Motion #
It declares no animation in its component source. Any transition comes from its stylesheet, which uses the duration tokens that collapse to instant under prefers-reduced-motion.