@agencecinq/disclosure-button
v2.0.0
Published
Accessible WAI-ARIA disclosure button Web Component.
Readme
@agencecinq/disclosure-button
Accessible WAI-ARIA disclosure button Web Component.
A disclosure button shows or hides a section of content. <cinq-disclosure-button>
wraps a <button>, wires up aria-expanded / aria-controls, toggles hidden on
controlled regions, and dispatches open/close events.
Implementation follows the
WAI-ARIA Authoring Practices disclosure pattern.
Inspired by @19h47/disclosure-button.
Installation
pnpm add @agencecinq/disclosure-buttonUsage
import "@agencecinq/disclosure-button";<cinq-disclosure-button>
<button
type="button"
aria-expanded="false"
aria-controls="details-1"
>
Show more
</button>
</cinq-disclosure-button>
<div id="details-1" class="foo" hidden>
Disclosure content
</div>.foo[hidden] {
display: none;
}
.foo {
display: flex;
}Importing the package registers the custom element. Controlled regions live
outside the host and are resolved via
ariaControlsElements.
HTML is the source of truth. The component will not auto-set
role, auto-migrate attributes, or warn about missing labels. Use an a11y linter (axe-core, Lighthouse) to catch invalid markup.
Required markup
| Attribute / element | Required | Role |
| ------------------- | -------- | ---- |
| Inner <button> | Yes | Focusable trigger inside <cinq-disclosure-button>. |
| aria-expanded | Yes | Current disclosure state on the trigger. |
| aria-controls | Yes | Space-separated ID(s) of the controlled region(s). |
| hidden | Yes | On each controlled region when collapsed. |
API
| Method | Description |
| ------ | ----------- |
| open(emit?) | Shows every controlled element. |
| close(emit?) | Hides every controlled element. |
| toggle() | Closes if any controlled element is visible; otherwise opens all. |
| update() | Syncs aria-expanded from the DOM (e.g. after an external dismiss). |
| destroy() | Removes listeners. |
| Property | Description |
| -------- | ----------- |
| $button | The inner <button>. |
| elements | Controlled elements from ariaControlsElements at connect time. |
| expanded | Reads aria-expanded on the trigger. Call update() after external DOM changes. |
open() / close() accept an optional emit flag (default true).
When several regions are controlled: aria-expanded is true while at least
one is visible; a trigger click closes all if any remain open.
Style via the trigger, e.g. button[aria-expanded="true"] or
cinq-disclosure-button:has([aria-expanded="true"]).
Keyboard & focus
The trigger must be a native <button type="button">. Enter and Space activate
it through the browser; the component listens for click. Focus stays on the
trigger when opening or closing. While collapsed, hidden keeps the region out
of the tab order and accessibility tree.
On focus / blur, a .focus class is toggled on the trigger:
cinq-disclosure-button button.focus {
outline: 2px solid currentColor;
outline-offset: 2px;
}One button, multiple targets
aria-controls is an ID reference list — one control may reference several
elements (see the ariaControlsElements MDN example).
After a partial dismiss (region.hidden = true), call update() so
aria-expanded stays honest.
Multiple buttons, one target
Several triggers can share the same ID. Use one <cinq-disclosure-button> per
trigger and sync sibling aria-expanded yourself from event.detail.open /
event.detail.el.
Programmatic API
const $host = document.querySelector("cinq-disclosure-button");
$host.open();
$host.close();
$host.toggle();
$host.update();$openLink.addEventListener("click", () => {
$host.open();
});
$dismissButton.addEventListener("click", () => {
$host.close();
});Events
| Event | Cancelable | Detail | Description |
| ----- | ---------- | ------ | ----------- |
| disclosure-button:open | Yes | { ids, elements, el, open: true } | Fired on the trigger before open. Cancel to abort. |
| disclosure-button:close | Yes | { ids, elements, el, open: false } | Fired on the trigger before close. Cancel to abort. |
import { EVENTS } from "@agencecinq/utils";
$host.addEventListener(EVENTS.DISCLOSURE_BUTTON_OPEN, (event) => {
if (!userMayOpen()) {
event.preventDefault();
}
});event.detail carries { ids, elements, el, open }. Events fire before the
DOM mutates — use detail.open rather than reading hidden yet.
Updating the button label
The component does not change the trigger’s visible text. Listen to open/close and update copy in your app:
import { EVENTS } from "@agencecinq/utils";
const labels = { closed: "Show details", open: "Hide details" };
$button.addEventListener(EVENTS.DISCLOSURE_BUTTON_OPEN, () => {
$button.textContent = labels.open;
});
$button.addEventListener(EVENTS.DISCLOSURE_BUTTON_CLOSE, () => {
$button.textContent = labels.closed;
});Build setup
pnpm -C packages/disclosure-button buildAcknowledgments
- Disclosure Pattern (WAI-ARIA Practices)
@19h47/disclosure-button— original implementation
See the interactive docs for live examples.
