# Accordion

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

- Status: beta
- Group: structure
- Page: https://www.digitaltableteur.com/design-system/components/accordion
- Contract: https://www.digitaltableteur.com/contracts/v1/Accordion.contract.json

## 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

| Prop | Type | Default | Description | Required |
| --- | --- | --- | --- | --- |
| `items` | `AccordionItem[]` | none | Accordion 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 |
| `defaultOpenId` | `string` | none | Initially open id (uncontrolled, single). | No |
| `defaultOpenIds` | `string[]` | none | Initially open ids (uncontrolled); wins over defaultOpenId when set. | No |
| `openIds` | `string[]` | none | Controlled open ids. When set, the parent owns open state. | No |
| `onOpenChange` | `(openIds: string[]) => void` | none | Called with the full set of open ids whenever it changes. | No |
| `headingLevel` | `2 \| 3 \| 4 \| 5 \| 6` | none | Wraps 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 |
| `className` | `string` | `""` | 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

| Key | Behaviour |
| --- | --- |
| Enter | Activates the focused control. (standard) |
| Space | Activates or toggles the focused control. (standard) |
| Tab | Moves 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.

| Level | Rule | How it is checked |
| --- | --- | --- |
| MUST NOT | `openIds` and `defaultOpenIds` set together. `openIds` makes the component controlled and `defaultOpenIds` uncontrolled; pass one, not both. | Checked by the MCP tool validate_component_usage. |
| MUST | ARIA: Aria-expanded on triggers. | Not yet verified: no evidence record. WCAG 4.1.2. |
| MUST | ARIA: Region panels. | Not yet verified: no evidence record. WCAG 4.1.2. |
| MUST | Required stories produce no axe violations at the configured severity. | Automated by `npm run test:stories:matrix:ci`. WCAG 2.5.8. |
| MUST | The 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. |
| MUST | The component remains legible and operable under a real browser forced-colors: active pass. | Automated by `npm run test:stories:hc`. |
| MUST | Every documented keyboard interaction works as specified. | Not yet verified: no evidence record. WCAG 2.1.1, 2.1.2. |
| MUST | A human completed an end-to-end accessibility review of this component. | Manual review, last on 2026-05-26. |
| SHOULD | Use it for scannable reference content (FAQs, specs) where one section at a time is the point. | Not stated in the contract. |
| SHOULD | Keep titles short questions or labels; the trigger is a button, not a heading replacement. | Not stated in the contract. |
| SHOULD | Set defaultOpenId to the question most visitors came for. | Not stated in the contract. |
| SHOULD | Keep ids stable; they key the open state and the trigger/region ARIA pairing. | Not stated in the contract. |
| SHOULD NOT | Do not hide task-critical content in an accordion; required steps must be visible by default. | Not stated in the contract. |
| SHOULD | Use 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 NOT | Do 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.

## Links

- Machine-readable contract (Design System Contract 1.0): https://www.digitaltableteur.com/contracts/v1/Accordion.contract.json
- Markdown version of this page: https://www.digitaltableteur.com/design-system/components/accordion.md
- Component index as JSON: https://www.digitaltableteur.com/design-system/components.json
- All components: https://www.digitaltableteur.com/design-system/components
- Source: The implementation lives in a private repository. This page, its Markdown twin and the contract are generated from it at build time and are the public source of truth.
