Primitives
Accordion
A stack of collapsible sections, optionally allowing multiple panels open at once.
Open a panel with the pointer or Enter, move between headers with the arrow keys, and watch data-state flip on the item, its trigger and its content together.
A library of headless UI primitives with built-in WAI-ARIA accessibility.
When to choose
- Accordion: a group of collapsible items under one root.
[(value)]holds which are open,multipledecides whether more than one may be, and ArrowUp / ArrowDown / Home / End move focus across the triggers. - Disclosure: a single trigger and its region, with no shared state and no arrow-key navigation. Stacking several of them is not an accordion, and that is the right shape when the panels are unrelated.
- Tabs: when exactly one panel is ever visible and the panels are alternatives rather than sections the reader may open together.
Anatomy
<div forAccordion>
<div forAccordionItem value="item-1">
<h3>
<button type="button" forAccordionTrigger>Trigger</button>
</h3>
<div forAccordionContent>Panel content</div>
</div>
<!-- repeat forAccordionItem per section -->
</div>
Examples
Multiple
multiple lets several sections stay open at once, so value holds an array of every open item.
Orders ship within 24 hours and arrive in three to five business days.
Guaranteed next-day delivery for orders placed before 2pm local time.
Horizontal
orientation='horizontal' lays the sections out in a row and switches roving navigation to ArrowLeft / ArrowRight. It is reflected as data-orientation for styling.
A summary of the workspace, recent activity and pending invitations.
Disabled item
A disabled item cannot be toggled and is skipped by the arrow keys, while staying in the DOM for screen readers.
Update your display name, email address and avatar.
API
ForAccordion
| Property | Type | Description |
|---|---|---|
value | model | Currently open item values. In single mode the array has 0 or 1 element. Default: — |
multiple | input | When true, multiple items can be open simultaneously. Default: false |
collapsible | input | Single mode only: when true, the open item can be collapsed by clicking it. Otherwise once any item is open, exactly one stays open. Default: false |
disabled | input | When true, disables every item: each trigger reflects the native disabled attribute and cannot toggle. Composes with a per-item [disabled].Default: false |
orientation | input | Layout direction of the trigger list. In horizontal mode ArrowLeft/Right replace ArrowUp/Down. Default: 'vertical' |
dir | input | Writing direction. Only relevant in horizontal mode, where it swaps the meaning of Left/Right arrows. Default: — |
| Data attribute | Values | |
|---|---|---|
data-orientation | horizontal | vertical | |
data-disabled | present | absent |
ForAccordionItem
| Property | Type | Description |
|---|---|---|
value | input.required | Unique identifier within the accordion. Required. Default: — |
disabled | input | When true, the trigger ignores clicks and exposes the native disabled attribute.Default: — |
| Data attribute | Values | |
|---|---|---|
data-state | open | closed | |
data-disabled | present | absent | |
data-orientation | horizontal | vertical |
ForAccordionTrigger
| Data attribute | Values | |
|---|---|---|
data-state | open | closed | |
data-orientation | horizontal | vertical |
ForAccordionContent
| Data attribute | Values | |
|---|---|---|
data-state | open | closed | |
data-orientation | horizontal | vertical |
Keyboard
| Key | Action | |
|---|---|---|
| Enter / Space | Toggle the focused trigger (native button). | |
| ArrowDown / ArrowUp | Move focus between triggers (vertical, default). Wrap-around, skips disabled. | |
| ArrowLeft / ArrowRight | Move focus between triggers (horizontal, flipped under dir='rtl'). Wrap-around, skips disabled. | |
| Home | Jump to the first trigger. | |
| End | Jump to the last trigger. |
Accessibility
- Heading wrapper is your job. The library does not render a heading around the trigger, so wrap it in the heading level (
<h2>–<h6>) appropriate to your document outline. Without it, screen-reader landmark navigation is broken. - Use a real
<button type="button">for the trigger. Native Enter / Space activation and focus come for free; the directive does not synthesize them. role="region"is added to every panel automatically. APG recommends suppressing it on accordions with 6+ panels to avoid landmark proliferation; there is currently no opt-out.- Closed panels leave the accessibility tree. While closed,
ForAccordionContentsetsaria-hidden="true"andinerton the panel, removing it from both the accessibility tree and the focus order. The directive does not apply[hidden], so pick how to hide it visually:- Mount / unmount with
@if (item.expanded()): the panel is absent from the DOM while closed; the cleanest path foranimate.enter/animate.leave. The trigger emitsaria-controlsonly while expanded, so the reference never dangles at an unmounted panel. - Leave it mounted: preserve internal state or run CSS-only transitions off
data-state. Adddisplay: none(or your own collapse animation) keyed on[data-state="closed"]to also hide it visually.
- Mount / unmount with
aria-disabledis applied to the open trigger only when single mode is active andcollapsible=false, indicating the user cannot collapse it from this trigger.- A truly disabled item (
[disabled]on[forAccordionItem]) uses the nativedisabledattribute on the trigger, by design. The trigger is a real single-purpose<button>, not a roving-tabindex collection item (each trigger stays independently in the Tab order; arrow-key navigation is the APG-optional enhancement on top). The disabled trigger leaves the Tab order and the arrow-key navigation (which already skips it), but stays in the accessibility tree so screen readers announce it as unavailable in browse mode. The APG Accordion pattern does not require disabled headers to remain focusable.
Styling
forty-cdk ships no styles: put your own class on each piece and key your CSS off the data-* attributes listed under API, not off the for* selectors (Styling forty-cdk explains why).
.chevron {
transition: transform 150ms ease;
}
.acc-trigger[data-state='open'] .chevron {
transform: rotate(180deg);
}
Wrapping in a design system
Subclass the root and re-provide FOR_ACCORDION_CONTEXT with useExisting pointing at the subclass, since Angular does not inherit a directive's providers; Wrapping non-form roots walks the pattern.