Skip to content

Accordion

LayoutSince 0.2.0
import { Accordion } from '@absoluteui/components/accordion';
import '@absoluteui/components/accordion/style.css';

Using Angular? See Accordion for Angular.

Vertically stacked headers that each show or hide a section of content, following the WAI-ARIA APG Accordion pattern. Every header is a native heading containing a native <button>, so Tab, Enter, and Space work without scripting tricks. Supports single or multiple expansion, an optional “always one open” mode, and an animated height transition that is removed under reduced motion.

  • Long pages where users need only some sections at a time, such as FAQs or settings groups.
  • Progressive disclosure of secondary details in a constrained space.
  • Content users must read or compare side by side: show it on the page.
  • Switching between peer views of the same content: use Tabs.
  • A single show/hide toggle: a native <details> element is enough.
  • Navigation menus: use a navigation component.
import { Accordion } from '@absoluteui/components/accordion';
const host = document.createElement('div');
document.body.append(host);
Accordion.create(host, {
headingLevel: 2,
value: ['shipping'],
items: [
{ id: 'shipping', title: 'How long does shipping take?', content: 'Orders ship within two business days.' },
{ id: 'returns', title: 'Can I return an item?', content: 'Returns are free for 30 days.' },
],
});

Several open panels with rich content and change tracking

Section titled “Several open panels with rich content and change tracking”
import { Accordion, type AccordionChangeDetail } from '@absoluteui/components/accordion';
const details = document.createElement('p');
details.textContent = 'Rich content is passed as a Node.';
const host = document.createElement('div');
document.body.append(host);
const accordion = Accordion.create(host, {
multiple: true,
items: [
{ id: 'profile', title: 'Profile', content: details },
{ id: 'billing', title: 'Billing', content: 'Billing settings', disabled: true },
],
});
host.addEventListener('aui-change', (event) => {
const { value } = (event as CustomEvent<AccordionChangeDetail>).detail;
host.dataset.open = value.join(',');
});
accordion.expand('profile');
import { configureAbsoluteUI } from '@absoluteui/core';
configureAbsoluteUI({ components: { accordion: { headingLevel: 2 } } });
import { Accordion, type AccordionChangeDetail } from '@absoluteui/components/accordion';
import '@absoluteui/components/accordion/style.css';
const host = document.querySelector<HTMLElement>('#settings');
if (host) {
Accordion.create(host, {
multiple: true,
items: [
{ id: 'profile', title: 'Profile', content: 'Name and photo' },
{ id: 'privacy', title: 'Privacy', content: 'Who can see your activity' },
],
});
host.addEventListener('aui-change', (event) => {
host.dataset.open = (event as CustomEvent<AccordionChangeDetail>).detail.value.join(',');
});
}
import { Accordion } from '@absoluteui/components/accordion';
const host = document.createElement('div');
document.body.append(host);
Accordion.create(host, {
headingLevel: 2,
items: [
{ id: 'shipping', title: 'How long does shipping take?', content: 'Orders ship within two business days.' },
{ id: 'returns', title: 'Can I return an item?', content: 'Returns are free for 30 days.' },
],
});
<div class="aui-accordion"> (consumer host)
<div class="aui-accordion__item [--expanded] [--disabled]">
<h3 class="aui-accordion__heading"> (headingLevel)
<button class="aui-accordion__trigger" aria-expanded aria-controls=panel id=trigger>
<span class="aui-accordion__title">Title</span>
<svg class="aui-icon aui-accordion__icon" aria-hidden="true">
</button>
</h3>
<div class="aui-accordion__panel" id=panel role="region" aria-labelledby=trigger hidden>
<div class="aui-accordion__clip">
<div class="aui-accordion__content">content</div>
</div>
</div>
</div>
</div>
Accordion.create(host: HTMLElement, options: AccordionOptions): Accordion

Global configuration key: accordion (see Configuration).

Option Type Default Description
items readonly { id: string; title: string; content: Node | string; disabled?: boolean }[] Items with unique ids. Titles and string content render as text. Required.
value readonly string[] "[]" Expanded item ids (at most one when multiple is false).
icons Partial<{ expand: IconRef }> Icon slot overrides.
multiple boolean false Allow several panels open at once; otherwise expanding one collapses the others. Globally configurable.
collapsible boolean true Single mode: the open panel can be collapsed. When false its header is aria-disabled. Globally configurable.
headingLevel 2 | 3 | 4 | 5 | 6 3 Heading element wrapping each header button. Globally configurable.
Property Type Description
host HTMLElement Consumer host; its children are owned by the component.
value string[] Expanded item ids, in item order.
options Readonly<AccordionOptions> Current instance options.
Method Description
panel(id: string): HTMLDivElement | null Content container of an item’s panel.
expand(id: string): void Expand programmatically (no events); collapses others in single mode.
collapse(id: string): void Collapse programmatically (no events), even when not collapsible.
toggle(id: string): void Expand or collapse programmatically (no events).
update(options: Partial<AccordionOptions>): void Merge and apply options.
destroy(): void Remove rendering, listeners, and subscriptions.
Event Detail Bubbles Cancelable Description
aui-change { value: string[]; id: string; expanded: boolean } Yes No The user expanded or collapsed an item.
Area Description
panel Item content (the content option, or accordion.panel(id)).

All rules live in the aui.components cascade layer, so application styles override them without specificity battles.

Class Purpose
aui-accordion Host.
aui-accordion--animated Set after the first toggle; enables the expand transition.
aui-accordion__item Item wrapper.
aui-accordion__item--expanded | --disabled Item state modifiers.
aui-accordion__heading Heading element (h2 to h6).
aui-accordion__trigger Header button.
aui-accordion__title Header text.
aui-accordion__icon Expand indicator (rotates when expanded).
aui-accordion__panel Panel (region when six items or fewer).
aui-accordion__clip Grid row that clips content during the height transition.
aui-accordion__content Content container.
Slot Default icon
expand chevron-down

Tokens this stylesheet reads (verified in CI). Override them in a theme or with the Theme Designer.

--aui-border-width | --aui-color-border | --aui-color-focus-ring | --aui-color-hover | --aui-color-text | --aui-color-text-disabled | --aui-color-text-muted | --aui-control-height-lg | --aui-duration-fast | --aui-duration-normal | --aui-easing-standard | --aui-focus-ring-offset | --aui-focus-ring-width | --aui-font-family-sans | --aui-font-size-md | --aui-font-size-sm | --aui-font-weight-medium | --aui-icon-size-sm | --aui-line-height-normal | --aui-line-height-tight | --aui-space-3 | --aui-space-4

type AccordionHeadingLevel = 2 | 3 | 4 | 5 | 6;
interface AccordionItem { id: string; title: string; content: Node | string; disabled?: boolean }
interface AccordionOptions { items: readonly AccordionItem[]; value?: readonly string[]; multiple?: boolean; collapsible?: boolean; headingLevel?: AccordionHeadingLevel; icons?: Partial<{ expand: IconRef }> }
interface AccordionChangeDetail { value: string[]; id: string; expanded: boolean }

Pattern: WAI-ARIA APG Accordion pattern: native heading elements containing native buttons that control panels.

Key Context Action
Tab / Shift+Tab Any Moves through header buttons and focusable panel content in document order.
Enter / Space Header button focused Expands or collapses the panel (native button activation).
ArrowDown / ArrowUp Header button focused Moves focus to the next or previous header, wrapping (APG optional).
Home / End Header button focused Moves focus to the first or last header (APG optional).
Element Role
heading heading (native h2 to h6, headingLevel)
header button button (native)
panel region when there are six items or fewer; otherwise none
indicator icon none (aria-hidden=true)
Attribute Element Value
aria-expanded header button “true” when the panel is shown, otherwise “false”
aria-controls header button Panel id
aria-disabled header button “true” for disabled items, and for the open item in single non-collapsible mode
aria-labelledby panel Header button id
hidden panel Present while collapsed

Header buttons show a :focus-visible inset outline. Disabled and locked headers stay focusable so their state is discoverable. Re-rendering keeps focus on the same header.

Criterion Level Note
1.3.1 Info and Relationships A
1.4.11 Non-text Contrast AA
2.1.1 Keyboard A
2.4.3 Focus Order A
2.4.6 Headings and Labels AA
2.4.7 Focus Visible AA
2.3.3 Animation from Interactions AAA
4.1.2 Name, Role, Value A

Automated test results: Accessibility and compliance.

  • Panels use role=region only for six items or fewer, following APG guidance against landmark proliferation.
  • Arrow keys are handled only on header buttons, never inside panel content.
  • The height transition and icon rotation animation are removed under prefers-reduced-motion and animation ‘none’.
  • Manual screen-reader verification (NVDA, JAWS, VoiceOver) is pending.

collapsed, expanded, expanded and locked (single, non-collapsible), disabled item, hover, focus-visible.

From Trigger To
collapsed user activates header expanded; in single mode the open item collapses; aui-change
expanded user activates header (collapsible or multiple) collapsed; aui-change
expanded (single, non-collapsible) user activates header no change (header is aria-disabled)
any expand / collapse / toggle / update({ value }) new state; no events
multiple, several open global config switches to single only the first open item stays expanded
  • Headers are real headings; set headingLevel to fit the page outline.
  • Disabled and locked headers stay focusable (aria-disabled, not disabled) so their state is discoverable.
  • Re-rendering (for example a new headingLevel) keeps focus on the same header.
  • The height transition and the icon rotation animation are removed under prefers-reduced-motion and animation: 'none'; the rotated icon remains as a static indicator.
  • Manual screen-reader verification is pending (see Known limitations).

Full width of its container. Long titles wrap; the indicator stays at the inline end.

Logical properties throughout. The title is at the inline start and the indicator at the inline end.

Colors come from role tokens only. In forced-colors mode borders use CanvasText, focus uses a Highlight outline, and disabled headers use GrayText.

configureAbsoluteUI({ components: { accordion: { multiple: true, headingLevel: 2 } } });

Changes apply to existing instances immediately.

Instance options override global and theme defaults. All options are dynamic.

icons.expand (default chevron-down); rotated 180 deg while expanded.

Titles and string content use textContent and are never parsed as HTML. Node content is inserted as-is; sanitizing it is the consumer’s responsibility. CSP-safe: no inline styles or handlers.

No persistence, network access, or telemetry.

Two delegated listeners on the host (click, keydown) and one configuration subscription, regardless of item count. State changes update attributes in place; only items, headingLevel, or icons changes re-create the item DOM. Budget: under 5 ms for 50 items (not yet covered by the performance test).

Designed for tens of items. Unit-tested with up to seven items.

  • value with more than one id in single mode throws ACCORDION_OPTIONS.
  • An empty items array renders an empty host.
  • Programmatic collapse is allowed in non-collapsible mode; only user interaction is locked.
  • A disabled item can still be expanded programmatically.
  • Node content is moved into the panel, not cloned; passing the same Node to two items keeps it in the last one.
Code Category Cause
ACCORDION_OPTIONS configuration Missing or duplicate ids, empty title, unknown ids in value, or several ids in single mode.
ACCORDION_ITEM configuration expand, collapse, or toggle with an unknown id.
CFG_INVALID_VALUE configuration Invalid multiple, collapsible, or headingLevel value.

A failed construction leaves no rendering, listeners, or subscriptions behind.

  • Collapsed content is not found by find-in-page (hidden, not hidden="until-found").
  • Changing items re-creates the item DOM.
  • Screen-reader behavior has not yet been manually verified with NVDA, JAWS, and VoiceOver.
  • HTML strings as titles or content (pass a Node).
  • Interactive content inside headers.