OGE logoOGE

Accordion

<oge-accordion> stacks disclosure panels that come from projected <oge-accordion-item> children, from a data-driven items array, or both. It follows the WAI-ARIA APG accordion pattern — each title is a <button> inside a heading and every header stays in the page Tab sequence — and layers arrow / Home / End / type-ahead navigation on top. Height animation, RTL and prefers-reduced-motion work out of the box.

APG pattern signals lazy render async guards

Declarative panels

Projected children carry their own content. selectedIndex is a two-way model for the single-expand case; a user gesture first fires the cancelable itemExpanding, then itemExpanded. collapsible lets a second click close the open panel — without it the last open panel deliberately stays open (see the third demo). Disabled panels are skipped by clicks and arrow keys.

[(selectedIndex)] collapsible badge disabled

Data-driven items

The items array drives the panels; expandedKeys is the multi-expand two-way model, so state survives reordering and insertions. icon takes raw SVG path data — there is no icon font or icon package. A component-level ogeAccordionContentTemplate renders every item's body.

items [(expandedKeys)] icon description

Body of General — expandedKeys: general

Single, multiple & collapsible

Single-expand collapses the sibling automatically. Without collapsible the last open panel cannot be closed, and the APG says such a header gets aria-disabled="true" — not disabled, so it stays focusable. Toggle the switches and watch the open header.

multiple collapsible aria-disabled

Lazy rendering & keep-alive

With deferRendering (default) a lazy ogeAccordionContentTemplate is instantiated on first expand; keepAlive (default) then keeps it mounted while collapsed — the creation time does not change when you reopen. Turn keep-alive off and the content is recreated every time.

deferRendering keepAlive

Async expand guard

Expanding runs a pipeline: cancelable itemExpanding → the panel's async expandGuard (the header shows a spinner, extra clicks are ignored) → itemExpanded. The guard also runs on collapse. Resolving false, throwing and rejecting all veto. The guarded panel here takes a second to confirm.

expandGuard single-flight rejection = veto

Invalid sections

Flag a panel invalid and it grows a danger rail, a dot beside the title and a visually hidden label so screen readers announce it. expandInvalid() opens every failing section at once — the natural move after a rejected form submit.

invalid expandInvalid()

Async content loader

A per-panel contentLoader runs on first expand: a shimmering skeleton shows while it is pending, the resolved value reaches the content template as data, and a rejection renders the failure message with a real retry button. The second panel fails once, then succeeds.

contentLoader skeleton retry

Header actions

The APG puts the panel title in a <button>, so a second focusable control cannot live inside it — axe flags that as nested-interactive. Header actions are therefore rendered as siblings of the toggle: real buttons, reachable with Tab, skipped by the accordion's arrow navigation.

ogeAccordionHeaderActionsTemplate no nested-interactive

Panel-level control

Each panel owns a two-way [(expanded)] and imperative open()/close()/toggle() — writes go through the same pipeline, so a guard veto reverts the binding. [ogeAccordionActionRow] is the footer action bar (the references' action-row slot), and afterExpand/afterCollapse fire once the height animation settles. Collapsing a panel that holds focus hands focus back to its header.

[(expanded)] open() / close() ogeAccordionActionRow afterExpand

Toggle position & styling

togglePosition is logical, so RTL mirrors it for free. displayMode: 'flat' drops the gutters and joins the panels into one stack, stylingMode switches between outlined, filled and borderless, and size sets the header density.

togglePosition displayMode stylingMode size