# Accordion

Package: @commenda/pathfinder@0.1.0
Page: /docs/components/accordion
Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=363-2

```tsx
import { Accordion, type AccordionItem } from '@commenda/pathfinder/accordion';

const filingSections: AccordionItem[] = [
  {
    value: 'scope',
    title: 'What the annual report covers',
    content:
      'We file the Delaware annual report, pay the franchise tax and send you the state’s confirmation.',
  },
  {
    value: 'due',
    title: 'When it is due',
    description: 'Delaware, due 1 March',
    content:
      'Complete your review by 15 February so there is time to prepare the filing.',
  },
  {
    value: 'details',
    title: 'What we need from you',
    badgeConfig: { label: 'Action needed', tone: 'warning' },
    content: (
      <label>
        Company reference{' '}
        <input aria-label="Company reference" placeholder="Add a reference" />
      </label>
    ),
  },
];

export const AccordionDefault = () => (
  <Accordion headingLevel={2} defaultValue={['scope']} items={filingSections} />
);
```


## Installation [#installation]

```sh
pnpm --filter your-app add "@commenda/pathfinder@workspace:*"
```

Import `@commenda/pathfinder/styles.css` once after host CSS. Pathfinder ships compiled, prefixed Tailwind v4 CSS; the application needs no Tailwind scanning. Import from `@commenda/pathfinder/accordion`. Interactive callbacks belong in a client component in Next.js App Router.

## API reference [#api-reference]

| Prop | Type | Requirement | Behavior |
| --- | --- | --- | --- |
| `items` | `readonly AccordionItem[]` | required | Required; value, title, content; optional description, badgeConfig, disableConfig |
| `variant` | `"card" \| "divided"` | optional | divided |
| `mode` | `"multiple" \| "single"` | conditional | multiple |
| `value` | `readonly string[] \| string \| null` | conditional | Array in multiple mode; string/null in single mode |
| `defaultValue` | `readonly string[] \| string \| null` | optional | Array in multiple mode; string/null in single mode |
| `onValueChange` | `((value: readonly string[]) => void) \| ((value: string \| null) => void)` | conditional | Required with value; follows mode type |
| `headingLevel` | `2 \| 3 \| 4 \| 5 \| 6` | optional | 3; choose for the surrounding document |
| `panelConfig` | `{ keepMounted?: boolean; }` | optional | true; retains embedded form values |
| `className` | `string` | optional | Additional classes for the component root. |
| `id` | `string` | optional | Stable HTML identifier for the component. |

### panelConfig fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `keepMounted` | `boolean` | No |  |

Root IDs, className, appropriate ARIA attributes, and typed refs are supported. Controlled value requires onValueChange and cannot be combined with defaultValue. Item IDs/values must be unique and stable.

## Usage rules [#usage-rules]

Multiple mode permits several open sections. Single mode opens at most one and allows closing it to `null`. All start closed unless defaultValue is supplied; open the first when it contains the main information.

The entire header is one button. `badgeConfig` is read-only; do not put links, checkboxes, or action buttons in the title. `disableConfig` groups isDisabled/reason. Tab visits headers in document order; Enter/Space toggles. No arrow-key roving focus. The chevron turns 180° and content height animates over 200ms, disabled with reduced motion.

Keep mounted panels preserve state while their content is hidden from focus. The application opens the section containing a form error before focusing that field. Do not nest accordions. A single Show more block belongs to a Collapsible pattern.

## Examples [#examples]

### Cards [#cards]


```tsx
import { Accordion, type AccordionItem } from '@commenda/pathfinder/accordion';

const filingSections: AccordionItem[] = [
  {
    value: 'scope',
    title: 'What the annual report covers',
    content:
      'We file the Delaware annual report, pay the franchise tax and send you the state’s confirmation.',
  },
  {
    value: 'due',
    title: 'When it is due',
    description: 'Delaware, due 1 March',
    content:
      'Complete your review by 15 February so there is time to prepare the filing.',
  },
  {
    value: 'details',
    title: 'What we need from you',
    badgeConfig: { label: 'Action needed', tone: 'warning' },
    content: (
      <label>
        Company reference{' '}
        <input aria-label="Company reference" placeholder="Add a reference" />
      </label>
    ),
  },
];

export const AccordionCards = () => (
  <Accordion
    headingLevel={2}
    variant="card"
    defaultValue={['scope']}
    items={filingSections.map((item, index) => ({
      ...item,
      title:
        ['Annual filing scope', 'Filing deadline', 'Company details'][index] ??
        item.title,
    }))}
  />
);
```


### Single [#single]


```tsx
import { Accordion, type AccordionItem } from '@commenda/pathfinder/accordion';

const filingSections: AccordionItem[] = [
  {
    value: 'scope',
    title: 'What the annual report covers',
    content:
      'We file the Delaware annual report, pay the franchise tax and send you the state’s confirmation.',
  },
  {
    value: 'due',
    title: 'When it is due',
    description: 'Delaware, due 1 March',
    content:
      'Complete your review by 15 February so there is time to prepare the filing.',
  },
  {
    value: 'details',
    title: 'What we need from you',
    badgeConfig: { label: 'Action needed', tone: 'warning' },
    content: (
      <label>
        Company reference{' '}
        <input aria-label="Company reference" placeholder="Add a reference" />
      </label>
    ),
  },
];

export const AccordionSingle = () => (
  <Accordion
    headingLevel={2}
    mode="single"
    defaultValue="scope"
    items={filingSections.map((item, index) => ({
      ...item,
      title:
        ['Included in the report', 'Submission date', 'Required information'][
          index
        ] ?? item.title,
    }))}
  />
);
```


### Disabled [#disabled]


```tsx
import { Accordion } from '@commenda/pathfinder/accordion';

export const AccordionDisabled = () => (
  <Accordion
    headingLevel={2}
    items={[
      {
        value: 'restricted',
        title: 'Billing details',
        content: 'Billing details',
        disableConfig: {
          isDisabled: true,
          reason: 'Administrator access required',
        },
      },
    ]}
  />
);
```


The header `badgeConfig` accepts `label`, `tone`, and `indicatorConfig`. Its size is fixed to the small Badge variant so callers cannot request a size the header silently overrides.

## Design rules [#design-rules]

Figma guidance and reviewed implementation decisions for this component. Rule IDs remain stable for source comments and AI references.

### Accordion [#accordion]

Source: [363:267](https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=363-267), item 363:159, docs 364:217.

- `ACCORDION-01` — Source: whole header toggles; Label/13 Strong title, optional Copy/12 description and read-only badge; trailing 16 chevron turns 180°. No interactive elements inside the toggle.
- `ACCORDION-02` — Source/Pathfinder: multiple open is default; single mode is for mutually exclusive sections. All closed unless initialized. Preserve form state by keeping closed content mounted by default, without leaving it focusable.
- `ACCORDION-03` — Source: no nesting. Show more for one long block is a Collapsible pattern. Divided header padding 10/8, 0.5 rule, hover darkens only chevron. Card header 14/16, radius 10, section gap 8. Height/chevron transitions 200ms; reduced motion immediate.
- `ACCORDION-04` — Pathfinder: heading/button/panel relationships, native Tab order, Enter/Space toggle. App opens sections containing form errors before focusing the field. disableConfig groups disabled state and its reason. Single values are string/null; multiple values are readonly string arrays.
