---
title: Accordion
description: Coordinate one or several expandable sections with shared disclosure rules.
package: moraine
version: 0.6.0
repository: https://github.com/subframe7536/moraine
---

# Accordion

> Coordinate one or several expandable sections with shared disclosure rules.

Use Accordion for several related disclosures that share one open-item policy. For a single independent disclosure, use [Collapsible](https://moraine.subf.dev/components/collapsible.md).

## Basic usage

```tsx
import { Accordion } from 'moraine'

export function Example() {
  return (
    <Accordion items={[{ value: 'shipping', label: 'Shipping', content: 'Ships in two days.' }]} />
  )
}
```

## Anatomy

```text
Accordion [component; slot=root]
└── item [slot]
    ├── header [slot]
    │   └── trigger [slot]
    │       ├── leading [slot]
    │       ├── label [slot]
    │       └── trailing [slot]
    └── content [slot]
        └── body [slot]
```

## Usage

### Open items

Use an array of values for `value`, `defaultValue`, and `onChange` in both modes. Single mode opens at most one item; set `multiple` to allow several. In controlled usage, pair `value` with `onChange`. `collapsible` determines whether the currently open item may be closed.

```tsx
import { Accordion } from 'moraine'
import { createSignal } from 'solid-js'

const FAQ_ITEMS = [
  {
    value: 'q1',
    label: 'Is Moraine SSR-compatible?',
    content: 'Yes, Moraine is designed for full SolidStart SSR and SSG hydration safety.',
  },
  {
    value: 'q2',
    label: 'How does styling work?',
    content:
      'Moraine uses atomic class styling with official presets and plugins for UnoCSS and Tailwind CSS.',
  },
]

export function OpenItems() {
  const [active, setActive] = createSignal<string[]>(['q1'])

  return (
    <div class="max-w-md w-full space-y-3">
      <Accordion collapsible items={FAQ_ITEMS} value={active()} onChange={setActive} />
      <p class="text-xs text-muted-foreground">
        Open item: <span class="text-foreground font-mono">{active()[0] ?? 'none'}</span>
      </p>
    </div>
  )
}
```

### Content lifetime

The content shell stays mounted so each trigger's `aria-controls` always points to a real element. `unmountOnHide` controls only the user content inside that shell. Choose based on whether hidden controls need to preserve local state.

```tsx
import { Accordion, Input } from 'moraine'

export function ContentLifetime() {
  return (
    <div class="max-w-md w-full">
      <Accordion
        collapsible
        defaultValue={['settings']}
        items={[
          {
            value: 'settings',
            label: 'Persistent form state',
            content: (
              <div class="py-2 space-y-2">
                <p class="text-xs text-muted-foreground">
                  Inputs retain their state across open/close toggles.
                </p>
                <Input placeholder="Preserved input text" defaultValue="Draft notes" />
              </div>
            ),
          },
        ]}
      />
    </div>
  )
}
```

### Keyboard interaction

| Key                                 | Description                                  |
| ----------------------------------- | -------------------------------------------- |
| <kbd>Enter</kbd> / <kbd>Space</kbd> | Toggles the focused trigger when enabled.    |
| <kbd>ArrowDown</kbd>                | Moves focus to the next enabled trigger.     |
| <kbd>ArrowUp</kbd>                  | Moves focus to the previous enabled trigger. |
| <kbd>Home</kbd>                     | Moves focus to the first enabled trigger.    |
| <kbd>End</kbd>                      | Moves focus to the last enabled trigger.     |

## Examples

### Single item

```tsx
import { Accordion } from 'moraine'
import { createSignal } from 'solid-js'

export function Single() {
  const [openValue, setOpenValue] = createSignal<string[]>(['invite'])

  return (
    <div class="max-w-xl w-full space-y-3">
      <Accordion
        value={openValue()}
        onChange={setOpenValue}
        items={[
          {
            value: 'invite',
            label: 'What should I check before inviting a member?',
            leading: 'i-lucide:user-plus',
            content:
              'Confirm the email address and choose the role that gives the person only the access they need.',
          },
          {
            value: 'archive',
            label: 'When should I archive a project?',
            leading: 'i-lucide:archive',
            content:
              'Archive work that no longer needs active updates after your team has saved any reports it still needs.',
          },
          {
            value: 'status',
            label: 'Where should a release status be recorded?',
            leading: 'i-lucide:list-checks',
            content:
              'Keep the status on the release record so everyone sees the same source of truth.',
          },
        ]}
      />

      <p class="text-xs text-muted-foreground">
        Active section: <span class="text-foreground font-medium">{openValue()[0] ?? 'none'}</span>
      </p>
    </div>
  )
}
```

### Multiple items

```tsx
import { Accordion } from 'moraine'

export function Multiple() {
  return (
    <Accordion
      multiple
      defaultValue={['general', 'security']}
      items={[
        {
          value: 'general',
          label: 'General Workspace Information',
          leading: 'i-lucide:settings',
          content: 'Configure your organization name, slug, avatar, and default language settings.',
        },
        {
          value: 'security',
          label: 'Security & Access Control',
          leading: 'i-lucide:shield-alert',
          content:
            'Manage SSO providers, enforce two-factor authentication, and review active sessions.',
        },
        {
          value: 'billing',
          label: 'Subscription & Invoices',
          leading: 'i-lucide:credit-card',
          content:
            'View current plan usage, payment methods, and download historical invoice receipts.',
        },
      ]}
      classes={{
        root: 'max-w-xl rounded-xl b-1 b-border border-border bg-card',
        trigger: 'px-4',
        content: 'px-4 text-foreground text-sm',
      }}
    />
  )
}
```

### Disabled and custom content

```tsx
import { Accordion } from 'moraine'

export function DisabledCustomContent() {
  return (
    <Accordion
      defaultValue={['setup']}
      trailing="icon-plus"
      items={[
        {
          value: 'setup',
          label: 'Setup checklist',
          leading: 'i-lucide-list-checks',
          content: (
            <div class="space-y-2">
              <p>Complete these steps before inviting your team:</p>
              <ul class="pl-5 list-disc space-y-1">
                <li>Create workspace profile</li>
                <li>Configure authentication</li>
                <li>Enable notifications</li>
              </ul>
              <div class="text-xs text-muted-foreground p-2 rounded-md bg-muted">
                Tip: You can finish the checklist later from Settings.
              </div>
            </div>
          ),
        },
        {
          value: 'security',
          label: 'Security review (Locked)',
          leading: 'i-lucide-shield-check',
          disabled: true,
          content: 'Available on Pro plan and above.',
        },
        {
          value: 'integrations',
          label: 'Integrations',
          leading: 'i-lucide-plug',
          content: (
            <div class="pt-2 space-y-2">
              <p>Connect your tools to automate the workflow.</p>
              <p>
                Supported: <strong>GitHub</strong>, <strong>Slack</strong>, and{' '}
                <strong>Notion</strong>.
              </p>
            </div>
          ),
        },
      ]}
      classes={{
        root: 'max-w-xl rounded-lg b-1 b-border border-border bg-background',
        trigger: 'px-3',
        content: 'px-4 text-foreground',
      }}
    />
  )
}
```

## Attributes

| Attributes | Slot | Description |
| --- | --- | --- |
| `data-disabled` | `accordion`, `accordion-item`, `accordion-trigger` | Present when the component, slot, or item is disabled. |
| `data-closed` | `accordion-item`, `accordion-trigger`, `accordion-content` | Present when disclosure or transition content is closed. |
| `data-expanded` | `accordion-item`, `accordion-trigger`, `accordion-trailing`, `accordion-content` | Present when the panel, accordion, or menu is expanded. |

## Props

Props for the Accordion component.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| collapsible | boolean \| undefined | true | Whether the last expanded item can be collapsed. |
| defaultValue | string[] \| undefined | [] | Default list of expanded item values for uncontrolled usage. |
| disabled | boolean \| undefined | false | Whether the entire accordion is disabled. |
| id | string \| undefined | — | Unique identifier for the accordion root element. |
| items | ({<br>  /** Header label for the accordion item. */<br>  label?: JSX.Element;<br>  /** Accessible label for the accordion trigger button.<br>Useful when `label` is omitted, icon-only, or contains non-text content. */<br>  ariaLabel?: string;<br>  /** Unique value for the accordion item. */<br>  value?: string;<br>  /** Whether the accordion item is disabled. */<br>  disabled?: boolean;<br>  /** Leading icon name for the accordion item. */<br>  leading?: IconT.Name;<br>  /** Content to display when the accordion item is expanded. */<br>  content?: JSX.Element;<br>  /** Optional class applied to the item element. */<br>  class?: SlotClassValue;<br>})[] \| undefined | — | Array of accordion items to render. |
| loop | boolean \| undefined | true | Whether arrow-key focus wraps from the last trigger to the first and vice versa. |
| multiple | boolean \| undefined | false | Whether multiple accordion items can be expanded at the same time. |
| onChange | ((value: string[]) => void) \| undefined | — | Callback when the expanded item values change. |
| trailing | IconT.Name \| undefined | 'icon-chevron-down' | Trailing icon name for all accordion items. |
| unmountOnHide | boolean \| undefined | true | Whether to unmount accordion content when hidden. |
| value | string[] \| undefined | — | Controlled list of expanded item values. |
| class | SlotClassValue \| undefined | — | Class applied to the component root or trigger element. |
| classes | Classes \| undefined | — | Family slot class defaults for this instance. |
| style | SlotStyleValue \| undefined | — | Style applied to the component root or trigger element. |
| styles | Styles \| undefined | — | Family slot style defaults for this instance. |
