Skip to content

panel

panel marks the region a disclosure trigger expands and collapses. aria-expanded tells assistive technology that something expands; it does not say what. Without the linkage a screen reader user hears that a control is expanded and must hunt for the content in reading order.

Status — React: Implemented • Web Components: Implemented • iOS: Not yet planned • Android: Not yet planned

This is the only way to supply that half of the pattern, because ids are generated at render and cannot be authored.

Roles

Apply the following roles to elements:

RoleTypeElement
panelPartElement of type: container or type: slot
WhereWhat is emitted
The panel elementA generated id
The disclosure triggeraria-controls={panelId}

The panel’s tag, classes, and children are unchanged. The role adds linkage, never show/hide logic.

Specs

Component anatomy typically has elements and roles like:

anatomy:
header:
type: container
role: disclosure
panel:
type: container
role: panel

Figma

Annotate the following layers:

  • panel as role:panel — on the region the trigger controls

It does not need to be a descendant of the trigger.

React

Implementation

Nothing. panel is wiring the component emits for itself; a consumer never addresses it.

Before / After

// before
<div className="accordion-panel" data-element="panel">{p.children}</div>
// after
<div className="accordion-panel" data-element="panel" id={panelId}>{p.children}</div>

The panel’s own diff is one attribute. The change that matters happens on the trigger, which gains aria-controls={panelId}.

Contract

None. panel adds an id to its own element and an attribute to a different one. The expansion event belongs to the trigger, which owns onExpandedChange.

The only parts carrying handlers are increment and decrement.

Web Components

Same emission. The id and the trigger’s aria-controls must be in the same shadow root — an id reference does not cross the boundary, so a panel arriving as slotted content from another component cannot be linked.

iOS

Not yet planned. Intended binding:

TypeThe disclosure’s content
LinkageStructural — no id reference exists or is needed

Android

Not yet planned. Intended binding:

TypeThe AnimatedVisibility content of the toggleable header
LinkageStructural

Additional details

Resolution

disclosure is not value-bearing, so panel resolves by proximity:

  1. The candidate disclosure whose subtree contains the panel, if there is one
  2. Otherwise the candidate that is the panel’s closest sibling-path ancestor’s child
SituationResult
Panel is a sibling of its triggerResolves — the usual shape, and what the proximity rule exists for
Two disclosures, ambiguous panelError naming the panel and every candidate
No disclosure in the componentValid. Emits the id, no wiring — the component provides a panel, and wiring arrives on composition

See also