Detent web components
By Shane Logsdon

Content & disclosure

Unfold Panel

Implemented

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

  • expanded recorded 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-multiple
  • variant

Properties

  • cards
  • expandedIds
  • onExpandedChange

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.tsx sha256 15962f25be00…

Native files

The native module and its artifacts:

  • src/components/unfold-panel/unfold-panel.ts
  • src/components/unfold-panel/styles.css
  • src/components/unfold-panel/define.ts
  • docs/components/unfold-panel.md
  • site/examples/unfold-panel.ts
  • tests/browser/unfold-panel.spec.ts
  • evidence/components/unfold-panel.md

API

Boolean attributes follow HTML presence semantics: present means true, absent means false, and disabled="false" still means disabled.

Attributes

Attributes
AttributePropertyTypeDefaultNotes
allow-multipleallowMultiplebooleanfalsePresence semantics: present means true, absent means false.
variantvariant'default' | 'minimal''default'Kebab-case attribute; camelCase property.

Properties

Properties
PropertyTypeDefaultNotes
cardsStackCard[]—Property-only: assign from script; not accepted as an attribute.
expandedIdsstring[]undefinedProperty-only: assign from script; not accepted as an attribute.
onExpandedChange(ids: string[]) => voidundefinedProperty-only: assign from script; not accepted as an attribute.

Events

Events
EventUpstream propPayload typeNotes
detent-expanded-changeonExpandedChange(ids: string[]) => voidComposed, 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.
Previous: SpringSelect Next: Stacked Cards

Docs

Components

Project