Accordion
stimeo--accordion
Collapsible sections that open independently, with the arrow keys moving between the headers.
Sections that open when you press their header. It implements the WAI-ARIA Accordion pattern, and several sections can stay open at once. Which header is currently open is announced to screen readers. The arrow keys move between the headers.
Ships within 2–3 business days.
Free returns within 30 days.
Covered by a 1-year limited warranty.
Keyboard
| Key | Action |
|---|---|
| Enter / Space | Toggle the focused section (native button). |
| ↓ / ↑ | Move focus between headers (wrapping). |
| Home / End | Focus the first / last header. |
<%# Markup for the accordion (APG Accordion) demo.
stimeo--accordion manages each header's aria-expanded and each panel's hidden,
and allows multiple panels open at once. Arrow keys move focus between headers.
Headers and panels are linked via aria-controls (= the panel's id), and each
panel (role="region") references its header button's id via aria-labelledby.
Each header also carries a "+" / "−" pair, and the library hides the half that
does not belong to its state. The glyphs are decorative, so they are aria-hidden
and the accessible name stays the section title. %>
<div class="accordion" data-controller="stimeo--accordion">
<h3 class="accordion__header">
<button type="button" id="acc-shipping-trigger" class="accordion__trigger"
aria-expanded="false" aria-controls="acc-shipping"
data-stimeo--accordion-target="trigger"
data-action="click->stimeo--accordion#toggle
keydown->stimeo--accordion#onKeydown">
<%= t("components.accordion.demo.shipping_title") %>
<span class="accordion__marker" aria-hidden="true"
data-stimeo--accordion-target="collapsedLabel">+</span>
<span class="accordion__marker" aria-hidden="true" hidden
data-stimeo--accordion-target="expandedLabel">−</span>
</button>
</h3>
<div class="accordion__panel" id="acc-shipping" role="region"
aria-labelledby="acc-shipping-trigger"
data-stimeo--accordion-target="panel" hidden>
<p><%= t("components.accordion.demo.shipping_desc") %></p>
</div>
<h3 class="accordion__header">
<button type="button" id="acc-returns-trigger" class="accordion__trigger"
aria-expanded="false" aria-controls="acc-returns"
data-stimeo--accordion-target="trigger"
data-action="click->stimeo--accordion#toggle
keydown->stimeo--accordion#onKeydown">
<%= t("components.accordion.demo.returns_title") %>
<span class="accordion__marker" aria-hidden="true"
data-stimeo--accordion-target="collapsedLabel">+</span>
<span class="accordion__marker" aria-hidden="true" hidden
data-stimeo--accordion-target="expandedLabel">−</span>
</button>
</h3>
<div class="accordion__panel" id="acc-returns" role="region"
aria-labelledby="acc-returns-trigger"
data-stimeo--accordion-target="panel" hidden>
<p><%= t("components.accordion.demo.returns_desc") %></p>
</div>
<h3 class="accordion__header">
<button type="button" id="acc-warranty-trigger" class="accordion__trigger"
aria-expanded="false" aria-controls="acc-warranty"
data-stimeo--accordion-target="trigger"
data-action="click->stimeo--accordion#toggle
keydown->stimeo--accordion#onKeydown">
<%= t("components.accordion.demo.warranty_title") %>
<span class="accordion__marker" aria-hidden="true"
data-stimeo--accordion-target="collapsedLabel">+</span>
<span class="accordion__marker" aria-hidden="true" hidden
data-stimeo--accordion-target="expandedLabel">−</span>
</button>
</h3>
<div class="accordion__panel" id="acc-warranty" role="region"
aria-labelledby="acc-warranty-trigger"
data-stimeo--accordion-target="panel" hidden>
<p><%= t("components.accordion.demo.warranty_desc") %></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 accordion demo.
* The library owns each header's aria-expanded, each panel's hidden, and the
* hidden of the header's two marker halves. The styling is built here.
*/
.accordion {
width: min(28rem, 100%);
border: 1px solid var(--border);
border-radius: 0.375rem;
}
.accordion__header {
margin: 0;
}
.accordion__trigger {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 0.75rem 1rem;
font-size: 1rem;
text-align: left;
cursor: pointer;
background: var(--surface-card);
border: 0;
border-bottom: 1px solid var(--border);
}
/* The "+" / "−" marker halves each header carries. */
.accordion__marker {
color: var(--muted);
}
.accordion__panel {
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--border);
}
.accordion__panel:last-child,
.accordion__header:last-of-type .accordion__trigger {
border-bottom: 0;
}
// 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--accordion']");
for (const name of ["open", "close"]) {
stateRoot?.addEventListener(`stimeo--accordion:${name}`, (event) => {
const reason = event.detail?.reason;
if (stateReadout) {
stateReadout.textContent = reason
? `stimeo--accordion:${name} · reason: ${reason}`
: `stimeo--accordion:${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--accordion"
Targets
| Name | Description | Attribute |
|---|---|---|
trigger
required
|
A header button that toggles its associated panel, paired to it via aria-controls. |
data-stimeo--accordion-target="trigger" |
panel
required
|
A collapsible region whose id is referenced by a trigger's aria-controls; its hidden attribute reflects state. |
data-stimeo--accordion-target="panel" |
expandedLabel
|
The half of a header shown while its panel is open; the controller owns its hidden. Resolved per header, paired with collapsedLabel in the same one. |
data-stimeo--accordion-target="expandedLabel" |
collapsedLabel
|
The half of a header shown while its panel is closed; the controller owns its hidden. Resolved per header, paired with expandedLabel in the same one. |
data-stimeo--accordion-target="collapsedLabel" |
Actions
| Name | Description | Action |
|---|---|---|
collapseAll
|
Closes every panel at once; bind to an optional "collapse all" control. | stimeo--accordion#collapseAll |
expandAll
|
Opens every panel at once; bind to an optional "expand all" control. | stimeo--accordion#expandAll |
onKeydown
|
Moves focus between headers: ArrowDown/Up cycle, Home/End jump to first/last. | stimeo--accordion#onKeydown |
toggle
|
Toggles the activated header's panel and syncs aria-expanded and the panel's hidden attribute. |
stimeo--accordion#toggle |
Events
| Name | Description | Event |
|---|---|---|
close
|
Fires once per pair that closed; detail carries the reason plus the index, trigger and panel. |
stimeo--accordion:close |
open
|
Fires once per pair that opened; detail carries the reason plus the index, trigger and panel. |
stimeo--accordion: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 |
Each header | "true" when its panel is open, "false" when collapsed. |
hidden |
Each panel | Removed when open, present when collapsed. |