Accordion
Vertically stacked sections that expand one (or several) at a time. For a single independent section use Collapsible; for switching between peer views use Tabs.
Added 14 October 2026
Accordion, Collapsible or Tabs?
- Accordion — a list of related sections where reading one at a time is fine (FAQs, settings groups, order line details). Items share one container and a divider rhythm.
- Collapsible — a single independent section with its own trigger; no sibling coordination.
- Tabs — peer views of the same subject where switching replaces the content; nothing stacks.
Single vs multiple open
Single-open is the default in both frameworks (React Base UI multiple defaults to
false; Angular brain type defaults to 'single'). Keep it — it preserves the
reader’s place and keeps the page height stable. Opt into multiple only when users
genuinely compare section contents side by side (React multiple, Angular
type="multiple").
Composition
React bakes the chevron pair into AccordionTrigger — pass only the label:
<Accordion defaultValue={["formats"]}>
<AccordionItem value="formats">
<AccordionTrigger>Which ad formats are supported?</AccordionTrigger>
<AccordionContent>
<p>Billboard, sitebar and mobile banner…</p>
</AccordionContent>
</AccordionItem>
</Accordion>
Open state is controlled on the root in React (defaultValue, value,
onValueChange — arrays of item values) but per item in Angular: isOpened for
default-open, [(isOpened)] / (isOpenedChange) for controlled use (brain has no
root-level value API). Both frameworks render the trigger inside an h3 by default;
Angular consumers may pick another heading level (brain accepts h1–h6), React’s is
fixed by Base UI.
Angular composes the chevron explicitly (ng-icon[hlmAccordionIcon], rotates when
open), and the trigger must be a <button> wrapped in a heading — brain enforces
this at runtime:
<div hlmAccordion>
<div hlmAccordionItem>
<h3 hlmAccordionHeader>
<button hlmAccordionTrigger>
Which ad formats are supported?
<ng-icon hlmAccordionIcon name="lucideChevronDown" aria-hidden="true" />
</button>
</h3>
<hlm-accordion-content>
<p>Billboard, sitebar and mobile banner…</p>
</hlm-accordion-content>
</div>
</div>
Keyboard
Tab/Shift-Tab and Enter/Space work in both frameworks. Arrow-key travel between triggers (plus Home/End) is Angular-only — brain ships a roving-focus key manager, while Base UI 1.6 dropped arrow-key navigation. Upstream behavior on both sides; don’t hand-roll arrows in React. [a11y]
Rules
- Trigger content is a label, not a control surface: no buttons, links or inputs
inside the trigger — they nest inside a
<button>and break semantics. [a11y] - Put borders/padding on elements inside the content, not on the content part
itself — the panel is height-clamped and clipped while animating, so its own
border-box would stay visible as it collapses. In React,
classNameonAccordionContentalready targets the inner padded div and is safe; in Angular,classon<hlm-accordion-content>lands on the clipped host — style an element inside instead. - Disable per item (
disabledonAccordionItem/hlmAccordionItem), and never let disabled be the only explanation — say why nearby. Note the frameworks differ: a disabled React trigger stays focusable and is announced (aria-disabled), a disabled Angular trigger leaves the Tab order entirely (nativedisabledfrom brain). [a11y] - Content is prose-styled (
text-sm, link underlines, paragraph spacing) — don’t rebuild those recipes per usage.