A disclosure widget that toggles visibility of a content panel. It animates open and close transitions using --collapsible-panel-height and --collapsible-panel-width CSS custom properties, supports hidden="until-found" for browser find-in-page, and exposes open state via events and a controller.
import { Collapsible } from "@areia/slots";
document.querySelector("#root")!.innerHTML = `
<div data-slot="collapsible" class="w-72">
<button data-slot="collapsible-trigger" class="win95-button">
Toggle
</button>
<div data-slot="collapsible-content" class="win95-inset mt-2 p-3">
Content revealed when the trigger is activated.
</div>
</div>
`;
const root = document.querySelector(
'[data-slot="collapsible"]',
)!;
const controller = Collapsible.createCollapsible(root, {
defaultOpen: false,
});
import { Collapsible } from "@areia/slots";
import { Collapsible } from "@areia/slots";
// Auto-bind a single root
const root = document.querySelector(
'[data-slot="collapsible"]',
)!;
const controller = Collapsible.createCollapsible(root, {
defaultOpen: false,
onOpenChange: (open) => console.log("open changed:", open),
});
// Auto-bind all unbound roots in scope
const controllers = Collapsible.create();
The collapsible expects a trigger button and a content panel inside the root.
<div data-slot="collapsible">
<button data-slot="collapsible-trigger">Toggle</button>
<div data-slot="collapsible-content">
Content revealed when the trigger is activated.
</div>
</div>
| Slot | Required | Description |
|---|
collapsible | Yes | Root container. |
collapsible-trigger | Yes | Button that toggles the content panel. |
collapsible-content | Yes | Panel whose visibility is controlled. |
The controller sets the following CSS custom properties on the content element to enable smooth open/close animations. During transitions the values are set to the content's pixel dimensions, and reset to auto once the transition settles.
| Property | Description |
|---|
--collapsible-panel-height | Pixels during open/close transition, auto when settled. |
--collapsible-panel-width | Pixels during open/close transition, auto when settled. |
Use these variables to animate height, width, block-size, or inline-size via CSS transitions:
[data-slot="collapsible-content"] {
overflow: hidden;
height: var(--collapsible-panel-height);
width: var(--collapsible-panel-width);
transition:
height 200ms ease-out,
width 200ms ease-out;
}
| Attribute | Element(s) | Description |
|---|
data-state | Root, content | "open" or "closed". |
aria-expanded | Trigger | "true" when open, "false" when closed. |
aria-controls | Trigger | Points to the content element id. |
aria-labelledby | Content | Points to the trigger element id. |
role | Content | Always set to "region". |
hidden | Content | Set to true when closed (or "until-found" when hiddenUntilFound is enabled). |
| Event | Detail | Description |
|---|
collapsible:change | { open: boolean } | Fired on the root when the open state changes. |
| Event | Detail | Description |
|---|
collapsible:set | { open: boolean } | Dispatch on the root to programmatically set the open state. |
| Option | Type | Default | Description |
|---|
defaultOpen | boolean | false | Initial open state. |
hiddenUntilFound | boolean | false | Use hidden="until-found" instead of hidden when closed, so browser find-in-page can reveal content. |
onOpenChange | (open: boolean) => void | — | Called when the open state changes. Not called on initial render. |
| Member | Type | Description |
|---|
open() | () => void | Open the collapsible. |
close() | () => void | Close the collapsible. |
toggle() | () => void | Toggle the collapsible. |
isOpen | boolean (getter) | Current open state. |
destroy() | () => void | Remove all listeners, observers, and clean up. |
The controller is returned by Collapsible.createCollapsible() and provides imperative control over the collapsible's state.
const controller = Collapsible.createCollapsible(root);
// Open programmatically
controller.open();
// Close programmatically
controller.close();
// Toggle
controller.toggle();
// Read current state
console.log(controller.isOpen); // true | false
// Clean up
controller.destroy();