# Accordion

```tsx
import { Accordion, useAccordionItem } from '@tangible/ui';
```

Interactive docs and examples: [Accordion in Storybook](https://storybook-tangible-ui.pages.dev/?path=/docs/components-accordion--docs).

## Accordion props

| Prop | Type | Default | Description |
| - | - | - | - |
| `children` | `ReactNode` | | |
| `type` | `"multiple" \| "single"` | | |
| `className?` | `string` | | |
| `collapsible?` | `boolean` | | Allow closing all items (otherwise one stays open) |
| `defaultValue?` | `string \| string[]` | | Uncontrolled: initially open item |
| `onValueChange?` | `((value: string \| undefined) => void) \| ((value: string[]) => void)` | | Callback when open item changes. Receives `undefined` when all items are closed (collapsible mode) — store it as `value ?? null` if your state is `string \| null`. |
| `value?` | `string \| string[]` | | Controlled: currently open item value. `null` means "controlled, nothing open". Controlled vs uncontrolled is decided once, at mount, from whether `value` is `undefined` — so pass `null` (not `undefined`) for "all closed", and if `undefined` does leak back in later (`onValueChange` reports it on collapse), it reads as "nothing open" rather than switching modes. |
| `variant?` | `"card" \| "flush"` | `'card'` | Visual treatment. `card` (default): Bordered cards with gap between items. `flush`: Items share borders, no gap, no radius — denser layout. |

### Accordion.Item

| Prop | Type | Default | Description |
| - | - | - | - |
| `children` | `ReactNode` | | |
| `value` | `string` | | Unique identifier for this item (required) |
| `className?` | `string` | | |
| `disabled?` | `boolean` | `false` | Prevent interaction |
| `headingLevel?` | `2 \| 3 \| 4 \| 5 \| 6` | | Wrap trigger in heading element (h2–h6). Omitting this reduces discoverability for screen reader users who navigate by headings (NVDA/JAWS `H` key). Only omit when the accordion is already inside an appropriate heading context. |

### Accordion.Trigger

| Prop | Type | Default | Description |
| - | - | - | - |
| `aria-label?` | `string` | | Accessible label for the trigger. Required when children is empty (e.g. icon-only triggers). |
| `asChild?` | `boolean` | `false` | When true, merges trigger props (aria attributes, event handlers, keyboard navigation) onto the child element instead of rendering a built-in button. The child must be a single React element that accepts ref and event handlers. The built-in chevron indicator is not rendered in asChild mode. |
| `children?` | `ReactNode` | | |
| `className?` | `string` | | |
| `unstyled?` | `boolean` | `false` | `asChild` only. Skip the `tui-accordion__trigger` class — the full-width row paint (flex, width 100%, padding, hover background, inset focus ring) — and leave the child's own styling alone. The behaviour hook (`data-tui-accordion-trigger`, `data-state`, ARIA, handlers) still lands. For a child that is already a styled control (an `IconButton`), not for a custom row that wants the accordion's look. |

### Accordion.Panel

| Prop | Type | Default | Description |
| - | - | - | - |
| `children` | `ReactNode` | | |
| `className?` | `string` | | |
| `landmark?` | `boolean` | `false` | Render panel as a landmark region (role="region"). Default false to avoid landmark pollution with many panels. |

## Hooks

### useAccordionItem

```ts
useAccordionItem(): AccordionItemState
```

Returns:

| Member | Type | Description |
| - | - | - |
| `value` | `string` | The item's `value`. |
| `isOpen` | `boolean` | Whether the item is currently open. |
| `disabled` | `boolean` | Whether the item is disabled — a secondary control should disable too. |
| `triggerId` | `string` | `id` of the item's trigger (the built-in one, or the `asChild` element). |
| `panelId` | `string` | `id` of the item's panel — the value for a secondary control's `aria-controls`. |
| `toggle` | `() => void` | Toggle this item, honouring `type`/`collapsible` and `disabled`. |

## Examples

- [Token Customisation](https://storybook-tangible-ui.pages.dev/?path=/story/components-accordion--token-customisation): Demonstrates the token API for fine-grained control over trigger vs panel
  appearance. All customisation is done via CSS custom properties on a
  wrapping element — no extra props needed.

  Tokens shown here:
  - `--tui-accordion-trigger-bg` / `--tui-accordion-trigger-padding`
  - `--tui-accordion-panel-bg` / `--tui-accordion-panel-padding`
  - `--tui-accordion-panel-spacing` (set to `0` to remove block breathing room)
  - `--tui-accordion-divider`

- [No Panel Block Spacing](https://storybook-tangible-ui.pages.dev/?path=/story/components-accordion--no-panel-spacing): Panel with `--tui-accordion-panel-spacing: 0` removes the top padding
  and bottom pseudo-element, useful when panel content provides its own
  padding (e.g. a padded card or list with built-in spacing).

- [Complex Header (Sibling Controls)](https://storybook-tangible-ui.pages.dev/?path=/story/components-accordion--complex-header): Demonstrates the correct pattern for accordion headers with interactive
  controls. The Trigger is a plain button (just the chevron) placed as a
  sibling to move handles, editable titles, and action buttons in a flex row.
  No nesting, no role hacks — each element is independent.

  Consumers can optionally stretch the trigger's click area across the full
  row with a `::after` pseudo-element if desired.

  **What to test:**
  - Chevron button toggles the panel
  - Action buttons log to the console **without** toggling
  - Title edit button enters edit mode **without** toggling
  - Move handle buttons work **without** toggling
  - Tab focuses each control independently in DOM order
  - Arrow Up/Down on the chevron navigates between accordion triggers
  - Disabled item is skipped during keyboard nav

- [asChild IconButton + Secondary Control](https://storybook-tangible-ui.pages.dev/?path=/story/components-accordion--as-child-icon-button): The repeater-row shape: the trigger is an `IconButton` via `asChild unstyled`, so it keeps its own
  sizing and focus ring instead of inheriting the full-width row paint (`tui-accordion__trigger`) —
  the behaviour hook (`data-tui-accordion-trigger`, `data-state`, ARIA, Enter/Space/arrow keys) still
  lands. Being a native `<button>`, Enter and Space toggle exactly once: the trigger leaves activation
  to the browser rather than synthesising a second one. An Edit/Close link beside it toggles the same
  item through `useAccordionItem()`, with its own `aria-expanded`/`aria-controls`. The accordion is
  controlled with `value: string | null` — `null` is "nothing open", not "uncontrolled". Note the
  chevron rotation belongs to the paint class, so an unstyled trigger shows open/closed by swapping
  the icon itself.

Other stories: Playground, Basic, Single (Non-Collapsible), Multiple, With Step Indicators, Course Syllabus, Controlled, Disabled Items, Keyboard Navigation, Flush Variant, Without Headings ([interactive page](https://storybook-tangible-ui.pages.dev/?path=/docs/components-accordion--docs)).

## Component API tokens

Read via fallback, never declared. Set one on an ancestor to restyle every instance under it.

- `--tui-accordion-bg`
- `--tui-accordion-bg-open`
- `--tui-accordion-fg`
- `--tui-accordion-border`
- `--tui-accordion-divider`
- `--tui-accordion-radius`
- `--tui-accordion-padding`
- `--tui-accordion-trigger-padding`
- `--tui-accordion-trigger-bg`
- `--tui-accordion-trigger-hover`
- `--tui-accordion-panel-padding`
- `--tui-accordion-panel-bg`
- `--tui-accordion-panel-spacing`
- `--tui-accordion-gap`
- `--tui-accordion-indicator`

---

Interactive version: https://storybook-tangible-ui.pages.dev/?path=/docs/components-accordion--docs
