Collapsible
A disclosure that shows and hides a single section of content.
@data-slot/collapsibleSource ↗bun add @data-slot/collapsiblenpm install @data-slot/collapsiblepnpm add @data-slot/collapsibleAnatomy
Both parts are required: a collapsible-trigger to toggle, and a collapsible-content to reveal.
<div data-slot="collapsible">
<button data-slot="collapsible-trigger">Toggle</button>
<div data-slot="collapsible-content">Content</div>
</div>
Examples
Show and hide a section
Collapsible is the simplest component — just a trigger and content.
Perfect for FAQ items, collapsible sections, or any show/hide pattern.
Collapsible is the simplest component — just a trigger and content.
Perfect for FAQ items, collapsible sections, or any show/hide pattern.
Show code
<div data-slot="collapsible">
<button data-slot="collapsible-trigger" class="collapsible-trigger-btn">
Show more details
</button>
<div class="collapsible-content-wrapper">
<div data-slot="collapsible-content" class="collapsible-content">
<div class="collapsible-content-inner">
<p>Collapsible is the simplest component — just a trigger and content.</p>
<p>Perfect for FAQ items, collapsible sections, or any show/hide pattern.</p>
</div>
</div>
</div>
</div>
<style>
.collapsible-trigger-btn {
padding: 0.5rem 1rem;
background: none;
border: 1px dashed var(--border);
cursor: pointer;
display: flex;
align-items: center;
gap: 0.5rem;
}
.collapsible-trigger-btn::before {
content: "▶";
font-size: 0.75rem;
transition: transform 0.2s;
line-height: 1rem;
}
.collapsible-trigger-btn[aria-expanded="true"]::before {
transform: rotate(90deg);
}
.collapsible-content-wrapper {
display: block;
}
.collapsible-content {
overflow: hidden;
min-height: 0;
height: var(--collapsible-panel-height);
transition: height 0.3s ease-out;
}
.collapsible-content[hidden] { display: block !important; }
.collapsible-content-inner {
padding: 1rem;
margin-top: 0.5rem;
background: var(--code-bg);
}
.collapsible-content[data-closed],
.collapsible-content[data-starting-style],
.collapsible-content[data-ending-style] { height: 0; }
.collapsible-content-inner p { margin: 0; }
.collapsible-content-inner p + p { margin-top: 0.5rem; }
</style><div data-slot="collapsible" class="group" style="--disclosure-icon: '▶'">
<button
data-slot="collapsible-trigger"
class="px-4 py-2 bg-transparent border border-dashed border-[var(--border)]
cursor-pointer flex items-center gap-2 hover:bg-code-bg
before:content-(--disclosure-icon) before:text-xs
before:transition-transform before:duration-200
aria-expanded:before:rotate-90"
>
Show more details
</button>
<div>
<div data-slot="collapsible-content" class="overflow-hidden h-(--collapsible-panel-height) transition-all duration-300 ease-out data-closed:h-0 data-starting-style:h-0 data-ending-style:h-0">
<div class="p-4 mt-2 bg-code-bg">
<p>Collapsible is the simplest component — just a trigger and content.</p>
<p class="mt-2">Perfect for FAQ items, collapsible sections, or any show/hide pattern.</p>
</div>
</div>
</div>
</div>import { create } from "@data-slot/collapsible";
// Initialize after the markup is in the document.
const controllers = create();
// Clean up before removing the component.
// controllers.forEach((controller) => controller.destroy());Open by default
Start expanded with data-default-open.
Collapsible is the simplest component — just a trigger and content.
Perfect for FAQ items, collapsible sections, or any show/hide pattern.
Collapsible is the simplest component — just a trigger and content.
Perfect for FAQ items, collapsible sections, or any show/hide pattern.
Show code
<div data-slot="collapsible" data-default-open>
<button data-slot="collapsible-trigger" class="collapsible-trigger-btn">
Show more details
</button>
<div class="collapsible-content-wrapper">
<div data-slot="collapsible-content" class="collapsible-content">
<div class="collapsible-content-inner">
<p>Collapsible is the simplest component — just a trigger and content.</p>
<p>Perfect for FAQ items, collapsible sections, or any show/hide pattern.</p>
</div>
</div>
</div>
</div>
<style>
.collapsible-trigger-btn {
padding: 0.5rem 1rem;
background: none;
border: 1px dashed var(--border);
cursor: pointer;
display: flex;
align-items: center;
gap: 0.5rem;
}
.collapsible-trigger-btn::before {
content: "▶";
font-size: 0.75rem;
transition: transform 0.2s;
line-height: 1rem;
}
.collapsible-trigger-btn[aria-expanded="true"]::before {
transform: rotate(90deg);
}
.collapsible-content-wrapper {
display: block;
}
.collapsible-content {
overflow: hidden;
min-height: 0;
height: var(--collapsible-panel-height);
transition: height 0.3s ease-out;
}
.collapsible-content[hidden] { display: block !important; }
.collapsible-content-inner {
padding: 1rem;
margin-top: 0.5rem;
background: var(--code-bg);
}
.collapsible-content[data-closed],
.collapsible-content[data-starting-style],
.collapsible-content[data-ending-style] { height: 0; }
.collapsible-content-inner p { margin: 0; }
.collapsible-content-inner p + p { margin-top: 0.5rem; }
</style><div data-slot="collapsible" data-default-open class="group" style="--disclosure-icon: '▶'">
<button
data-slot="collapsible-trigger"
class="px-4 py-2 bg-transparent border border-dashed border-[var(--border)]
cursor-pointer flex items-center gap-2 hover:bg-code-bg
before:content-(--disclosure-icon) before:text-xs
before:transition-transform before:duration-200
aria-expanded:before:rotate-90"
>
Show more details
</button>
<div>
<div data-slot="collapsible-content" class="overflow-hidden h-(--collapsible-panel-height) transition-all duration-300 ease-out data-closed:h-0 data-starting-style:h-0 data-ending-style:h-0">
<div class="p-4 mt-2 bg-code-bg">
<p>Collapsible is the simplest component — just a trigger and content.</p>
<p class="mt-2">Perfect for FAQ items, collapsible sections, or any show/hide pattern.</p>
</div>
</div>
</div>
</div>import { create } from "@data-slot/collapsible";
// Initialize after the markup is in the document.
const controllers = create();
// Clean up before removing the component.
// controllers.forEach((controller) => controller.destroy());API reference
Initialization
create(scope?)
Auto-discover and bind all collapsible instances in a scope (defaults to document).
import { create } from "@data-slot/collapsible";
const controllers = create(); // Returns CollapsibleController[]
createCollapsible(root, options?)
Create a controller for a specific element.
import { createCollapsible } from "@data-slot/collapsible";
const collapsible = createCollapsible(element, {
defaultOpen: false,
hiddenUntilFound: false,
onOpenChange: (open) => console.log(open),
});
Slots
Runtime Slots
collapsible- Root element that manages the open state.collapsible-trigger- Required button that toggles the content and receivesaria-expanded.collapsible-content- Required panel that is shown or hidden with the open state.
Markup
<div data-slot="collapsible">
<button data-slot="collapsible-trigger">Toggle</button>
<div data-slot="collapsible-content">Content</div>
</div>
Data Attributes
Options can also be set via data attributes on the root element. JS options take precedence.
| Attribute | Type | Default | Description |
|---|---|---|---|
data-default-open |
boolean | false |
Initial open state |
data-hidden-until-found |
boolean | false |
Use hidden="until-found" when closed |
Boolean attributes: present or "true" = true, "false" = false, absent = default.
<!-- Start expanded -->
<div data-slot="collapsible" data-default-open>
...
</div>
Options
| Option | Type | Default | Description |
|---|---|---|---|
defaultOpen |
boolean |
false |
Initial open state |
hiddenUntilFound |
boolean |
false |
Use hidden="until-found" when closed |
onOpenChange |
(open: boolean) => void |
undefined |
Callback when open state changes (not called on init) |
Controller
| Method/Property | Description |
|---|---|
open() |
Open the collapsible |
close() |
Close the collapsible |
toggle() |
Toggle the collapsible |
isOpen |
Current open state (readonly boolean) |
destroy() |
Cleanup all event listeners |
Events
Outbound Events
Listen for changes via custom events:
element.addEventListener("collapsible:change", (e) => {
console.log("Collapsible open:", e.detail.open);
});
Inbound Events
Control the collapsible via events:
| Event | Detail | Description |
|---|---|---|
collapsible:set |
{ open: boolean } |
Set open state programmatically |
// Open the collapsible
element.dispatchEvent(
new CustomEvent("collapsible:set", { detail: { open: true } })
);
// Close the collapsible
element.dispatchEvent(
new CustomEvent("collapsible:set", { detail: { open: false } })
);
Note: Blocked when trigger is disabled (has disabled attribute or aria-disabled="true").
Deprecated Shapes
The following shape is deprecated and will be removed in v1.0:
// Deprecated: { value: boolean }
element.dispatchEvent(
new CustomEvent("collapsible:set", { detail: { value: true } })
);
Use { open: boolean } instead.
Styling
Use data-state attributes for CSS styling (available on both root and content):
/* Hidden state */
[data-slot="collapsible-content"][hidden] {
display: none;
}
/* Or use data-state */
[data-slot="collapsible"][data-state="closed"] [data-slot="collapsible-content"] {
display: none;
}
/* Animate using data-state on content */
[data-slot="collapsible-content"] {
overflow: hidden;
transition: max-height 0.3s;
max-height: 0;
}
[data-slot="collapsible-content"][data-state="open"] {
max-height: 500px;
}
/* Presence lifecycle markers */
[data-slot="collapsible-content"][data-starting-style] {
opacity: 0;
}
[data-slot="collapsible-content"][data-ending-style] {
opacity: 0;
}
CSS Variables
The content element exposes size variables you can use for dimension animations:
| Variable | Description |
|---|---|
--collapsible-panel-height |
Panel height (auto at open rest, measured px during transitions, 0px when closed) |
--collapsible-panel-width |
Panel width (auto at open rest, measured px during transitions, 0px when closed) |
Example:
[data-slot="collapsible-content"] {
overflow: hidden;
height: var(--collapsible-panel-height);
width: var(--collapsible-panel-width);
transition: height 0.25s ease, width 0.25s ease, opacity 0.2s ease;
}
[data-slot="collapsible-content"][data-starting-style],
[data-slot="collapsible-content"][data-ending-style] {
height: 0;
opacity: 0;
}
Accessibility
The component automatically handles:
aria-expandedstate on triggeraria-controlslinking trigger to contentrole="region"on contentaria-labelledbylinking content back to trigger- Unique ID generation for trigger and content
- Disabled trigger support (respects
disabledattribute andaria-disabled="true")
Behavior
Find-in-Page Support
Enable hiddenUntilFound (or data-hidden-until-found) to close panels with
hidden="until-found". This allows browser find-in-page to reveal matching text.
With Tailwind:
<div data-slot="collapsible" class="group">
<button data-slot="collapsible-trigger" class="flex items-center gap-2">
<span>Show more</span>
<svg class="group-data-[state=open]:rotate-180 transition-transform">...</svg>
</button>
<div data-slot="collapsible-content" class="hidden group-data-[state=open]:block">
Content here
</div>
</div>