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. |
| 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 |
8px |
Corner radius of the outer container. |
--x-collapse-border |
1px solid #e2e8f0 |
Border of the outer container. |
--x-collapse-bg |
#ffffff |
Background of the outer container. |
--x-collapse-trigger-bg |
#f8fafc |
Background of the trigger button. |
--x-collapse-trigger-bg-hover |
#f1f5f9 |
Trigger background on hover. |
--x-collapse-trigger-color |
#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 |
0.9375rem |
Font size of the trigger label. |
--x-collapse-font-weight |
600 |
Font weight of the trigger label. |
--x-collapse-chevron-color |
#64748b |
Colour of the chevron indicator. |
--x-collapse-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."]]