forty-cdk
llms.txt

Primitives

Accordion

A stack of collapsible sections, optionally allowing multiple panels open at once.

forty-cdk/accordion WAI-ARIA APG

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, multiple decides 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

PropertyTypeDescription
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 attributeValues
data-orientationhorizontal | vertical
data-disabledpresent | absent

ForAccordionItem

PropertyTypeDescription
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 attributeValues
data-stateopen | closed
data-disabledpresent | absent
data-orientationhorizontal | vertical

ForAccordionTrigger

Data attributeValues
data-stateopen | closed
data-orientationhorizontal | vertical

ForAccordionContent

Data attributeValues
data-stateopen | closed
data-orientationhorizontal | vertical

Keyboard

KeyAction
Enter / SpaceToggle the focused trigger (native button).
ArrowDown / ArrowUpMove focus between triggers (vertical, default). Wrap-around, skips disabled.
ArrowLeft / ArrowRightMove focus between triggers (horizontal, flipped under dir='rtl'). Wrap-around, skips disabled.
HomeJump to the first trigger.
EndJump 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, ForAccordionContent sets aria-hidden="true" and inert on 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 for animate.enter / animate.leave. The trigger emits aria-controls only 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. Add display: none (or your own collapse animation) keyed on [data-state="closed"] to also hide it visually.
  • aria-disabled is applied to the open trigger only when single mode is active and collapsible=false, indicating the user cannot collapse it from this trigger.
  • A truly disabled item ([disabled] on [forAccordionItem]) uses the native disabled attribute 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.