Accordion
An accordion shows and hides sections of content. <cinq-accordion> wires up
header buttons, aria-expanded / aria-controls relationships, optional height
transitions, keyboard navigation between headers, and open/close events.
Inspired by @19h47/accordion.
Install
Section titled “Install”pnpm add @agencecinq/accordionImport once:
import "@agencecinq/accordion";Then write the WAI-ARIA markup explicitly. The component adds interactivity only.
HTML is the source of truth. The component will not auto-set
role, auto-migrate attributes, or warn about missing labels. Run an a11y linter (axe, Lighthouse) to catch invalid markup.
<cinq-accordion> <div data-panel data-open="true" data-deselect="true"> <button type="button" data-header id="scores-header" aria-expanded="true" aria-controls="scores-body" > Ability scores </button>
<div id="scores-body" role="region" aria-labelledby="scores-header"> <div data-inner>Roll 3d6 for each ability and assign the results.</div> </div> </div>
<div data-panel data-open="false"> <button type="button" data-header id="class-header" aria-expanded="false" aria-controls="class-body" > Class & kit </button>
<div id="class-body" role="region" aria-labelledby="class-header" hidden> <div data-inner>Check minimum ability scores, then pick a kit for starting gear.</div> </div> </div></cinq-accordion>Required markup
Section titled “Required markup”| Selector / attribute | Required | Role |
|---|---|---|
<cinq-accordion> |
Yes | Accordion container. |
[data-panel] |
Yes | Panel wrapper for each section. |
[data-header] or button |
Yes | Focusable trigger inside each panel. |
aria-expanded |
Yes | Current panel state on the trigger. |
aria-controls |
Yes | ID of the controlled region. |
role="region" |
Yes | On each panel body element. |
aria-labelledby |
Yes | On each region, referencing the header id. |
hidden |
Yes | On collapsed regions in their initial state. |
[data-inner] |
Recommended | Inner wrapper used for height measurement. |
Options
Section titled “Options”| Attribute | Default | Description |
|---|---|---|
data-multiselectable |
false |
Allow multiple panels open at once. |
data-hash |
true |
Open the panel whose region id matches the URL hash. |
Per panel:
| Attribute | Default | Description |
|---|---|---|
data-deselect |
false |
Allow collapsing when already open. |
data-open |
- | Reflected open state for styling. |
Nested accordion
Section titled “Nested accordion”You can nest a <cinq-accordion> inside a panel body. Each instance only
controls its own direct panels. Inner headers are excluded from the outer
keyboard group, and arrow keys stay scoped to the focused accordion.
Set data-hash="false" on nested hosts to avoid hash collisions.
<cinq-accordion data-hash="false"> <div data-panel data-open="true" data-deselect="true"> <button type="button" data-header id="character-header" aria-expanded="true" aria-controls="character-body" > Character creation </button>
<div id="character-body" role="region" aria-labelledby="character-header"> <div data-inner> <p>Roll ability scores, pick a class, then flesh out your adventurer.</p>
<cinq-accordion data-hash="false"> <div data-panel data-open="true" data-deselect="true"> <button type="button" data-header id="scores-header" aria-expanded="true" aria-controls="scores-body" > Ability scores </button>
<div id="scores-body" role="region" aria-labelledby="scores-header"> <div data-inner>Roll 3d6 for each ability...</div> </div> </div> </cinq-accordion> </div> </div> </div></cinq-accordion>Keyboard support
Section titled “Keyboard support”| Key | Function |
|---|---|
Tab / Shift + Tab |
Move focus through focusable elements. |
Space / Enter |
Toggle the focused header (native button). |
Arrow Up / Arrow Down |
Move focus between headers. |
Arrow Left / Arrow Right |
Move focus between headers. |
Home / End |
Focus first / last header. |
| Method | Description |
|---|---|
init() |
Binds panels and listeners (also runs on connectedCallback). |
destroy() |
Detaches listeners and panel controllers. |
closeAll() |
Closes every panel without emitting events. |
| Property | Description |
|---|---|
panels |
Internal panel controller instances. |
options |
Resolved multiselectable and hash options. |
When you mutate the panel DOM at runtime, re-bind with destroy(), mutate,
init() (same pattern as tabs).
Events
Section titled “Events”| Event | Cancelable | Detail | Description |
|---|---|---|---|
accordion:open |
Yes | { el, index } |
Fired before a panel opens. |
accordion:close |
Yes | { el, index } |
Fired before a panel closes. |
import { EVENTS } from "@agencecinq/utils";
$accordion.addEventListener(EVENTS.ACCORDION_OPEN, (event) => { console.log(event.detail.index);});Examples
Section titled “Examples”Basic accordion
One panel open at a time. Use arrow keys to move between headers,Space / Enter to toggle.
Roll 3d6 for each ability, Strength, Dexterity, Constitution, Intelligence, Wisdom, and Charisma. Then assign the totals.
Check minimum ability scores for your chosen class. Elves, dwarves, and halflings adjust scores per the Player's Handbook.
Starting gold and gear come from your class and kit tables. Weapons, armour, packs, and a handful of gold pieces.
Last event: -
Multiselectable
Set data-multiselectable="true" on the host to keep multiple panels open.
Extra weapon slots beyond your class list. Long sword, bow, or staff.
Languages, riding, and other NWP slots from your class and Intelligence score.
Nested accordion
Place a second <cinq-accordion> inside a panel body. Each instance only binds its own panels. Keyboard navigation stays scoped to the focused accordion. Disable hash sync on nested hosts with data-hash="false".
Roll ability scores, pick a class, then flesh out your adventurer.
Roll 3d6 for each ability, assign to Strength, Dexterity, Constitution, Intelligence, Wisdom, and Charisma.
Check minimum ability scores for your class, then choose a kit for extra proficiencies and starting equipment.
Starting gear comes from your class and background tables. Weapons, armour, packs, and a handful of gold pieces.
Deselect disabled
Without data-deselect="true", clicking an open header does not collapse it. Typical accordion behaviour.
Try clicking this header again. It stays open until you select another class.
Opens when selected and closes the first panel. Typical dual-class flow.
Destroy & recreate
Section titled “Destroy & recreate”const accordion = document.querySelector("cinq-accordion");
accordion.destroy(); // unbindaccordion.init(); // re-bindDestroy / Create
Useful when you mutate the DOM and want to re-bind events. Pattern: destroy(), mutate,init().
Trolls regenerate 3 hp per round unless burned or acid-treated.
Looks like a boulder until it animates. Then the fists start flying.
Status : Bound. Headers respond
Responsive layout (list ↔ accordion)
Section titled “Responsive layout (list ↔ accordion)”On narrow viewports you may want every section visible as a stacked list. On
desktop, the same markup becomes an accordion. Toggle with destroy() /
init() (here emulated by buttons. Production code usually uses
matchMedia).
const accordion = document.querySelector("cinq-accordion");const mq = window.matchMedia("(min-width: 768px)");
const expandAsList = () => { accordion.querySelectorAll("[data-panel]").forEach((panel) => { const header = panel.querySelector("[data-header]"); const body = document.getElementById(header.getAttribute("aria-controls")); panel.setAttribute("data-open", "true"); header.setAttribute("aria-expanded", "true"); body.removeAttribute("hidden"); });};
const apply = () => { if (mq.matches) { accordion.init(); } else { accordion.destroy(); expandAsList(); }};
mq.addEventListener("change", apply);apply();Responsive layout
Emulate a breakpoint: on “mobile” the accordion is unbound with destroy() and reads as a stacked list; on “desktop” init() restores accordion behaviour. In production, drive this with matchMedia.
Roll 3d6 for each ability and assign the results to Strength, Dexterity, Constitution, Intelligence, Wisdom, and Charisma.
Check minimum ability scores for the class, then pick a kit for starting gear and specialisation.
Pick Lawful, Neutral, or Chaotic on the ethical axis, and Good, Neutral, or Evil on the moral axis.
Mode : Desktop. Accordion bound
Add panels at runtime
Section titled “Add panels at runtime”const accordion = document.querySelector("cinq-accordion");
accordion.destroy();
accordion.insertAdjacentHTML( "beforeend", `<div data-panel data-open="false"> <button type="button" data-header id="cloaker-header" aria-expanded="false" aria-controls="cloaker-body" > Cloaker </button> <div id="cloaker-body" role="region" aria-labelledby="cloaker-header" hidden> <div data-inner>Hangs from the ceiling like a shadow.</div> </div> </div>`,);
accordion.init();Add panels at runtime
The pattern is: destroy(), mutate DOM,init().
The galeb duhr is a curious boulder-like creature with appendages that act as hands and feet.
Trolls are horrid carnivores found in all climes, from arctic wastelands to tropical jungles.