Collapsible
stimeo--collapsible
Opens and closes one region in place, and hands its height to CSS so you can animate it.
Opens and closes a single region from one button. Whether it is open is announced to screen readers. Animating a height usually fails because the content's height is unknown, so this measures the real height and hands it to CSS for you to animate. On closing, the region is hidden only after the animation finishes, so the movement is never cut short.
Orders placed before 2pm ship the same business day. Standard delivery takes 3–5 business days; express options are shown at checkout.
Keyboard
| Key | Action |
|---|---|
| Enter / Space | Toggle the region open or closed (native button). |
<%# Markup for the collapsible (APG Disclosure) demo.
The trigger opens/closes a single region inline. The library syncs aria-expanded /
hidden / data-state and the content's measured-height variable; the look (the
height transition) lives in demo.css.
The trigger carries both of its wordings as a label pair, so the words live in the
markup where they can be translated. The library shows the half that belongs to the
current state and hides the other, so demo.css has nothing to do here. The chevron
is decoration and is kept out of the accessible name. %>
<div class="collapsible" data-controller="stimeo--collapsible">
<button
class="collapsible__trigger"
type="button"
aria-expanded="false"
aria-controls="collapsible-demo-content"
data-stimeo--collapsible-target="trigger"
data-action="click->stimeo--collapsible#toggle">
<span class="collapsible__chevron" aria-hidden="true"></span>
<span data-stimeo--collapsible-target="collapsedLabel">
<%= t("components.collapsible.demo.show") %>
</span>
<span data-stimeo--collapsible-target="expandedLabel" hidden>
<%= t("components.collapsible.demo.hide") %>
</span>
</button>
<div
class="collapsible__content"
id="collapsible-demo-content"
data-state="closed"
hidden
data-stimeo--collapsible-target="content">
<p class="collapsible__body"><%= t("components.collapsible.demo.body") %></p>
</div>
</div>
<%# Readout: what the component just reported. Purely visual — the component
already announces what a screen reader needs, so this line is aria-hidden to
keep it from being read twice. The value is a code fragment (API identifiers
and values), so only the label is translated. %>
<p class="demo-readout" aria-hidden="true">
<span><%= t("catalog.readout.events") %></span>
<span class="demo-readout__value" data-readout>—</span>
</p>
/*
* Presentation-only styles for the collapsible demo.
* The open/close animation uses the data-state the library sets on the content
* target and --stimeo--collapsible-content-height (the content's measured height).
*/
.collapsible {
max-width: 32rem;
border: 1px solid var(--border-default);
border-radius: 0.5rem;
overflow: hidden;
}
.collapsible__trigger {
display: flex;
align-items: center;
gap: 0.5rem;
width: 100%;
padding: 0.75rem 1rem;
background: var(--surface-subtle);
border: 0;
font: inherit;
font-weight: 600;
color: var(--fg);
cursor: pointer;
text-align: left;
}
.collapsible__chevron {
width: 0.5rem;
height: 0.5rem;
border-right: 2px solid currentColor;
border-bottom: 2px solid currentColor;
transform: rotate(-45deg);
transition: transform 0.2s ease;
}
.collapsible__trigger[aria-expanded="true"] .collapsible__chevron {
transform: rotate(45deg);
}
/* Transition the height while opening/closing or open (i.e. once hidden is removed). */
.collapsible__content {
height: 0;
overflow: hidden;
transition: height 0.2s ease;
}
.collapsible__content[data-state="open"] {
height: var(--stimeo--collapsible-content-height);
}
.collapsible__body {
margin: 0;
padding: 1rem;
}
/* Disable the transition under reduced motion (fall back to instant open/close). */
@media (prefers-reduced-motion: reduce) {
.collapsible__content,
.collapsible__chevron {
transition: none;
}
}
// Every move of the open state is reported, with a `reason` saying what drove it
// — a trigger, Escape, a click outside, or a call from your own code. A real app
// subscribes to mirror the state elsewhere or to send an analytics event; this
// demo writes the event name and its reason into the readout so both are visible
// while you interact. `reason: "api"` is the one to watch for: a close you asked
// for comes back to your own listener.
// The page chrome uses some of these controllers too (the nav, the tab strip),
// so everything below is scoped to the demo itself.
const demoStage = document.querySelector(".demo-stage") ?? document;
const stateReadout = demoStage.querySelector("[data-readout]");
const stateRoot = demoStage.querySelector("[data-controller~='stimeo--collapsible']");
for (const name of ["open", "close"]) {
stateRoot?.addEventListener(`stimeo--collapsible:${name}`, (event) => {
const reason = event.detail?.reason;
if (stateReadout) {
stateReadout.textContent = reason
? `stimeo--collapsible:${name} · reason: ${reason}`
: `stimeo--collapsible:${name}`;
}
});
}
These demo styles use shared design tokens (light + dark). Copy the shared styles too, then toggle data-theme on your root element for dark mode.
The data-* attributes you add to your own HTML to wire this component. Put the data-controller below on a root element, then place its targets / values / actions inside that element.
On the root element
data-controller="stimeo--collapsible"
Targets
| Name | Description | Attribute |
|---|---|---|
trigger
required
|
The button that toggles the single disclosure region; carries aria-expanded. |
data-stimeo--collapsible-target="trigger" |
content
required
|
The inline region that expands/collapses; its data-state, hidden, and height variable are driven by the controller. |
data-stimeo--collapsible-target="content" |
expandedLabel
|
The half of the trigger shown while open; the controller owns its hidden. Pairs with collapsedLabel inside the same trigger. |
data-stimeo--collapsible-target="expandedLabel" |
collapsedLabel
|
The half of the trigger shown while closed; the controller owns its hidden. Pairs with expandedLabel inside the same trigger. |
data-stimeo--collapsible-target="collapsedLabel" |
Values
| Name | Description | Attribute |
|---|---|---|
open
|
Initial open state reflected onto the DOM on connect without animating (default false). | data-stimeo--collapsible-open-value |
Actions
| Name | Description | Action |
|---|---|---|
toggle
|
Toggles the region open/closed (animated), ordering hidden/data-state/height so the transition can run. |
stimeo--collapsible#toggle |
Events
| Name | Description | Event |
|---|---|---|
close
|
Fires as soon as the close starts; detail.reason says what moved it, and the close transition is not waited for. |
stimeo--collapsible:close |
open
|
Fires right after it opens; detail.reason says what moved it. |
stimeo--collapsible:open |
State hooks
The library only manages these ARIA/data attributes and custom properties. Your CSS reads them to render the look — selectors like [aria-selected], [aria-expanded], or var(--stimeo--…) hook into this state.
| Hook | Target | Meaning |
|---|---|---|
aria-expanded |
Trigger | "true" when open, "false" when closed. |
hidden |
Content | Present when closed (added after the close transition). |
data-state |
Content | "open" / "closed" — the height-animation starting point. |
--stimeo--collapsible-content-height |
Content | The content's measured height (px) for the height transition. |