Skip to main content

Design system components

Accordion

Coordinated disclosure group: single or multiple open sections, animated reveal, keyboard-operable triggers.

Status: beta. Group: structure. Markdown version: /design-system/components/accordion.md

When to use

Accordion coordinates a set of disclosure sections. In the default type="single" at most one stays open (opening another closes the previous); type="multiple" lets any number stay open for side-by-side reading. Pass items ({id, title, content}); drive it uncontrolled with defaultOpenId/defaultOpenIds or controlled with openIds + onOpenChange. variant switches between a bordered card group (contained) and flush hairlines (divided). For a single independent "show more" use ExpandableSection; if most readers need all the content, use plain headings instead of hiding it.

When not to use

  • Do not put primary navigation inside an accordion. AT users land on a closed panel and assume the link is gone.
  • Do not nest accordions more than one level deep. Two-level disclosure becomes unreadable in screen reader linear mode.
  • Do not ship long, scrollable panels. If the content scrolls inside.

Look-alikes

  • ExpandableSection: Do not build exclusive groups from several ExpandableSections; that is Accordion's contract.
  • Tabs: Do not use Tabs for non-mutually-exclusive content. Two panels open at once is a job for Accordion (in a future mode="multi") or side-by-side layout.

Props

Accordion props
PropTypeDefaultDescriptionRequired
itemsAccordionItem[]noneAccordion sections (id, title, content, optional disabled).Yes
type"single" | "multiple"single"single" (default) keeps at most one section open; opening another closes the previous. "multiple" lets any number stay open for side-by-side reading.No
variant"contained" | "enclosed" | "divided"contained"contained" (default) is a bordered card group with dividers between rows; "enclosed" is the same outer border but seamless inside (no dividers); "divided" is flush with hairline separators only, for inline disclosure.No
defaultOpenIdstringnoneInitially open id (uncontrolled, single).No
defaultOpenIdsstring[]noneInitially open ids (uncontrolled); wins over defaultOpenId when set.No
openIdsstring[]noneControlled open ids. When set, the parent owns open state.No
onOpenChange(openIds: string[]) => voidnoneCalled with the full set of open ids whenever it changes.No
headingLevel2 | 3 | 4 | 5 | 6noneWraps each trigger in a heading of this level, so the section titles join the page outline (screen reader users list and jump by headings). Pick the level below the heading that introduces the accordion. Unset, triggers are plain buttons.No
classNamestring""Optional utility classes on the container.No

Variants

  • Available types: single, multiple (default single).
  • Available variants: contained, enclosed, divided (default contained).
  • Allowed headingLevel values: 2, 3, 4, 5, 6.

Invalid combinations

  • MUST NOT: openIds and defaultOpenIds set together. openIds makes the component controlled and defaultOpenIds uncontrolled; pass one, not both.

Keyboard

Accordion keyboard behaviour
KeyBehaviour
EnterActivates the focused control. (standard)
SpaceActivates or toggles the focused control. (standard)
TabMoves focus to the control and on to the next focusable element. (standard)

Rows marked (standard) name a key the contract lists without a description; the behaviour shown is the usual one for that key.

Rules

Every item under When not to use is a MUST NOT. MUST and SHOULD follow RFC 2119.

Accordion rules
LevelRuleHow it is checked
MUST NOTopenIds and defaultOpenIds set together. openIds makes the component controlled and defaultOpenIds uncontrolled; pass one, not both.Checked by the MCP tool validate_component_usage.
MUSTARIA: Aria-expanded on triggers.Not yet verified: no evidence record. WCAG 4.1.2.
MUSTARIA: Region panels.Not yet verified: no evidence record. WCAG 4.1.2.
MUSTRequired stories produce no axe violations at the configured severity.Automated by npm run test:stories:matrix:ci. WCAG 2.5.8.
MUSTThe accessibility tree (role, name, state) matches the committed snapshot for every required story.Automated by npm run test:stories:matrix:ci. WCAG 1.3.1, 4.1.2.
MUSTThe component remains legible and operable under a real browser forced-colors: active pass.Automated by npm run test:stories:hc.
MUSTEvery documented keyboard interaction works as specified.Not yet verified: no evidence record. WCAG 2.1.1, 2.1.2.
MUSTA human completed an end-to-end accessibility review of this component.Manual review, last on 2026-05-26.
SHOULDUse it for scannable reference content (FAQs, specs) where one section at a time is the point.Not stated in the contract.
SHOULDKeep titles short questions or labels; the trigger is a button, not a heading replacement.Not stated in the contract.
SHOULDSet defaultOpenId to the question most visitors came for.Not stated in the contract.
SHOULDKeep ids stable; they key the open state and the trigger/region ARIA pairing.Not stated in the contract.
SHOULD NOTDo not hide task-critical content in an accordion; required steps must be visible by default.Not stated in the contract.
SHOULDUse type="multiple" when users compare sections side by side (feature lists, pricing tiers); keep the default type="single" for FAQs and settings where one at a time is the point.Not stated in the contract.
SHOULD NOTDo not put a single short paragraph behind a section; the click costs more than it saves; show the text directly.Not stated in the contract.

Install and import

  • Install: npm install @digitaltableteur/react (version 0.9.1, restricted access on npm).
  • Import: import { Accordion } from "@digitaltableteur/react";
  • Entry point: @digitaltableteur/react/layout exports it too.
  • Styles: import @digitaltableteur/react/style.css once at the app boundary.
  • Also available as the custom element <dt-accordion>; see the custom elements manifest.