A disclosure widget that shows or hides a content panel via an animated toggle. The trigger is a native button with a rotating chevron indicator. Height animation transitions from 0 to scrollHeight on open and back on close, with a transitionend / setTimeout fallback for robustness. All motion respects prefers-reduced-motion.
<x-collapse header="Section title">
<div slot="content">Panel content goes here.</div>
</x-collapse>
| Attribute | Type | Default | Description |
|---|
open | boolean presence | absent = closed | When present, the panel is expanded. |
disabled | boolean presence | absent = enabled | When present, the trigger button is inert and pointer-events are suppressed. |
header | string | "" | Text rendered inside the trigger button. |
duration-ms | number (0–2000) | 300 | Animation duration in milliseconds. Values outside the range are clamped. |
Attributes that belong together. This component acts at the moment an attribute is written: it starts its animation. It uses the other attributes as they are at that moment. When you change several attributes that belong together, write them in one step, so the component sees one state. See "The hold of a render" in UTILITIES.md.
| Property | Type | Reflects attribute |
|---|
open | boolean | open (boolean presence) |
disabled | boolean | disabled (boolean presence) |
header | string | header |
durationMs | number | duration-ms |
| Event | Bubbles | Composed | Cancelable | Detail |
|---|
x-collapse-toggle | yes | yes | yes | { open: boolean, source: "pointer" \| "keyboard" \| "programmatic" } |
x-collapse-change | yes | yes | no | { open: boolean } |
x-collapse-toggle fires before the state change. If it is cancelled (preventDefault()), the state change and the subsequent x-collapse-change event are both suppressed.
source is "pointer" for mouse/touch clicks, "keyboard" for Space or Enter key activation, or "programmatic" when toggle() is called from JavaScript.
| Slot | Description |
|---|
content | Content to show and hide. Use <... slot="content">. The element is always in the DOM; visibility is controlled by height and overflow. |
| Part | Description |
|---|
container | Outermost wrapper <div>. |
trigger | The <button> that controls open/close. |
header-text | <span> inside the trigger that renders the header attribute value. |
chevron | <span> inside the trigger containing the ▼ indicator. Rotates 180° when open. |
content | Animated <div> whose height transitions. overflow: hidden. |
content-inner | Inner <div> inside content that wraps the slot. |
| Variable | Default | Description |
|---|
--x-collapse-border-radius | var(--x-radius-md, 8px) | Corner radius of the outer container. |
--x-collapse-border | var(--x-border-width,1px) solid var(--x-color-border,#e2e8f0) | Border of the outer container. |
--x-collapse-bg | var(--x-color-surface, #ffffff) | Background of the outer container. |
--x-collapse-trigger-bg | var(--x-color-surface-hover, #f8fafc) | Background of the trigger button. |
--x-collapse-trigger-bg-hover | var(--x-color-surface-active,#f1f5f9) | Trigger background on hover. |
--x-collapse-trigger-color | var(--x-color-text, #0f172a) | Trigger text colour. |
--x-collapse-trigger-padding | 0.75rem 1rem | Padding inside the trigger button. |
--x-collapse-content-padding | 1rem | Padding inside the content panel. |
--x-collapse-font-size | var(--x-font-size-sm,0.9375rem) | Font size of the trigger label. |
--x-collapse-font-weight | var(--x-font-weight-semibold,600) | Font weight of the trigger label. |
--x-collapse-chevron-color | var(--x-color-text-muted, #64748b) | Colour of the chevron indicator. |
--x-collapse-focus-ring | var(--x-color-focus-ring, #60a5fa) | Inset focus ring colour on the trigger. |
- The trigger is a native
<button> element — fully keyboard accessible by default. aria-expanded on the trigger reflects the current open state ("true" / "false").aria-controls="panel" on the trigger points to the id of the content panel (scoped inside shadow DOM).aria-disabled="true" is set on the trigger when the disabled attribute is present; the button also gains the disabled HTML attribute so it is skipped by tab order.- The content panel has
role="region" and aria-labelledby="trigger" pointing back to the trigger (scoped inside shadow DOM). - When
prefers-reduced-motion: reduce is in effect, the height transition duration is forced to 0ms.
| Key | Condition | Action |
|---|
Space | Trigger focused, not disabled | Toggles the panel open/closed. |
Enter | Trigger focused, not disabled | Toggles the panel open/closed. |
<!-- Collapsed by default -->
<x-collapse header="Details">
<p slot="content">This content is hidden until the header is clicked.</p>
</x-collapse>
<!-- Open by default -->
<x-collapse header="Open section" open>
<p slot="content">This content is visible on load.</p>
</x-collapse>
<!-- Disabled -->
<x-collapse header="Locked section" disabled>
<p slot="content">Cannot be toggled.</p>
</x-collapse>
<!-- Custom duration -->
<x-collapse header="Slow animation" duration-ms="800">
<p slot="content">Animates over 800ms.</p>
</x-collapse>
<!-- Cancel toggle conditionally -->
<x-collapse id="guarded" header="Guarded panel">
<p slot="content">Toggle can be prevented.</p>
</x-collapse>
<script>
document.getElementById('guarded').addEventListener('x-collapse-toggle', e => {
if (!confirm('Allow toggle?')) e.preventDefault();
});
</script>
;; Collapsed by default
[:x-collapse {:header "Details"}
[:p {:slot "content"} "This content is hidden until the header is clicked."]]
;; Open by default
[:x-collapse {:header "Open section" :open true}
[:p {:slot "content"} "This content is visible on load."]]
;; Disabled
[:x-collapse {:header "Locked section" :disabled true}
[:p {:slot "content"} "Cannot be toggled."]]
;; Custom duration
[:x-collapse {:header "Slow animation" :duration-ms "800"}
[:p {:slot "content"} "Animates over 800ms."]]
;; Listen to events
[:x-collapse
{:header "With events"
:on-x-collapse-toggle #(js/console.log "toggle" (.. % -detail))
:on-x-collapse-change #(js/console.log "change" (.. % -detail))}
[:p {:slot "content"} "Panel content."]]