Content & disclosure
Unfold Panel
Progressive disclosure card stack with spring height animation
detent-unfold-panel
Port status
- Catalog status
- implemented
- Origin
- upstream
- Phase
- 2
- Wave
- phase-2-disclosure
- Group
- Content & disclosure
6.9 KB gzip for the class module
Variants
The first card is open. Press Enter or Space on a header to unfold it; the main example closes the previous card, the first panel above keeps several open, and the second uses the underlined minimal variant. Reduced motion reaches the final height immediately.
Recorded states
expandedrecorded state; no setter
Usage
Import
import '/components/unfold-panel/define.js';
// or register it yourself
import { DetentUnfoldPanel } from '/components/unfold-panel/unfold-panel.js';
Attributes
allow-multiplevariant
Properties
cardsexpandedIdsonExpandedChange
Slots
No slots are recorded for this component.
Events
detent-expanded-change— { ids: string[] }
Example
Runnable example: site/examples/unfold-panel.ts. The snippet below is generated from the catalogued attributes.
<detent-unfold-panel
allow-multiple
variant="default"
></detent-unfold-panel>
Property-only values
const element = document.querySelector('detent-unfold-panel');
if (element !== null) {
// cards: StackCard[]
element.cards = [];
// expandedIds: string[]
element.expandedIds = [];
// onExpandedChange: (ids: string[]) => void
element.onExpandedChange = () => {};
}
Source
The pinned upstream source for this row:
src/components/ui/StackUnfoldPanel.tsxsha256 15962f25be00…
Native files
The native module and its artifacts:
src/components/unfold-panel/unfold-panel.tssrc/components/unfold-panel/styles.csssrc/components/unfold-panel/define.tsdocs/components/unfold-panel.mdsite/examples/unfold-panel.tstests/browser/unfold-panel.spec.tsevidence/components/unfold-panel.md
API
Boolean attributes follow HTML presence semantics: present means true, absent means false, and disabled="false" still means disabled.
Attributes
| Attribute | Property | Type | Default | Notes |
|---|---|---|---|---|
allow-multiple | allowMultiple | boolean | false | Presence semantics: present means true, absent means false. |
variant | variant | 'default' | 'minimal' | 'default' | Kebab-case attribute; camelCase property. |
Properties
| Property | Type | Default | Notes |
|---|---|---|---|
cards | StackCard[] | — | Property-only: assign from script; not accepted as an attribute. |
expandedIds | string[] | undefined | Property-only: assign from script; not accepted as an attribute. |
onExpandedChange | (ids: string[]) => void | undefined | Property-only: assign from script; not accepted as an attribute. |
Events
| Event | Upstream prop | Payload type | Notes |
|---|---|---|---|
detent-expanded-change | onExpandedChange | (ids: string[]) => void | Composed, bubbles; fires only for a user action. |
Slots
No slots are recorded for this component.
CSS customisation
- React callback props become typed composed CustomEvents; programmatic state assignment emits none.
- Framer Motion springs/AnimatePresence ported to CSS transitions, WAAPI or the shared rAF spring helper.
- `lucide-react` icon components replaced with local SVG assets (ISC notice retained).
- React state/hooks replaced by element-owned state with pre-upgrade property preservation.
- Upstream defect corrected: `toggleCard` only wrote internal state when no callback was passed, so a callback without `expandedIds` froze the stack; the port always updates element-owned state when uncontrolled and treats the hook as a listener.
- Accessibility correction (PC-12.6): the decorative ordinal is `aria-hidden` (so it is not part of the accessible name) and the open panel gains `role="region"` plus `aria-labelledby`.
- `content` is text.
- The body's springSmooth (200/28/0.85) height/opacity presence runs on the shared spring helper; a folded panel is hidden (upstream unmounts it) and inert while folding. The Framer layout and entrance animation are not carried over (the stack never reorders); the chevron turns 180 degrees on springSnappy through the shared spring helper.
- `cards` defaults to `[]`, so an empty stack renders nothing.
What it does
A vertically stacked card deck that unfolds progressively: the first card is open, activating another card closes it unless allow-multiple is set.
Upstream behaviour
- Progressive vertical expansion with GPU-accelerated height calculation
- Automatic single-card accordion mode or multi-card unfolding
- Rotating chevron indicators with smooth 180-degree spring flip
- Full accessibility with aria-expanded, aria-controls, and keyboard triggers
Differences from upstream
- React callback props become typed composed CustomEvents; programmatic state assignment emits none.
- Framer Motion springs/AnimatePresence ported to CSS transitions, WAAPI or the shared rAF spring helper.
- `lucide-react` icon components replaced with local SVG assets (ISC notice retained).
- React state/hooks replaced by element-owned state with pre-upgrade property preservation.
- Upstream defect corrected: `toggleCard` only wrote internal state when no callback was passed, so a callback without `expandedIds` froze the stack; the port always updates element-owned state when uncontrolled and treats the hook as a listener.
- Accessibility correction (PC-12.6): the decorative ordinal is `aria-hidden` (so it is not part of the accessible name) and the open panel gains `role="region"` plus `aria-labelledby`.
- `content` is text.
- The body's springSmooth (200/28/0.85) height/opacity presence runs on the shared spring helper; a folded panel is hidden (upstream unmounts it) and inert while folding. The Framer layout and entrance animation are not carried over (the stack never reorders); the chevron turns 180 degrees on springSnappy through the shared spring helper.
- `cards` defaults to `[]`, so an empty stack renders nothing.