---
name: accordion
type: core
domain: navigation
requires: [loke-ui]
description: >
  Expandable section groups with type=single (exclusive) or type=multiple (independent).
  AccordionItem/Header/Trigger/Content composition. CSS variable animation via
  --loke-accordion-content-height. Keyboard navigation (Home/End/Arrow keys).
  collapsible prop only applies to type=single. AccordionContent proxies
  --loke-collapsible-content-height into --loke-accordion-content-height.
---

# Accordion

## Setup

Single-mode accordion — one item open at a time, closeable.

```tsx
import {
  Accordion,
  AccordionItem,
  AccordionHeader,
  AccordionTrigger,
  AccordionContent,
} from "@loke/ui/accordion";

function FaqAccordion() {
  return (
    <Accordion type="single" collapsible defaultValue="item-1">
      <AccordionItem value="item-1">
        <AccordionHeader>
          <AccordionTrigger>What is @loke/ui?</AccordionTrigger>
        </AccordionHeader>
        <AccordionContent>
          A headless React UI primitives library with granular subpath exports.
        </AccordionContent>
      </AccordionItem>

      <AccordionItem value="item-2">
        <AccordionHeader>
          <AccordionTrigger>Is it accessible?</AccordionTrigger>
        </AccordionHeader>
        <AccordionContent>
          Yes. Keyboard navigation, ARIA attributes, and roving focus are built in.
        </AccordionContent>
      </AccordionItem>
    </Accordion>
  );
}
```

## Core Patterns

### Multiple mode

All items can be open simultaneously. The `collapsible` prop is irrelevant here — items are always independently togglable.

```tsx
<Accordion type="multiple" defaultValue={["item-1", "item-3"]}>
  <AccordionItem value="item-1">
    <AccordionHeader>
      <AccordionTrigger>Section A</AccordionTrigger>
    </AccordionHeader>
    <AccordionContent>Content A</AccordionContent>
  </AccordionItem>

  <AccordionItem value="item-2">
    <AccordionHeader>
      <AccordionTrigger>Section B</AccordionTrigger>
    </AccordionHeader>
    <AccordionContent>Content B</AccordionContent>
  </AccordionItem>
</Accordion>
```

### CSS variable animation

`AccordionContent` measures the content and sets `--loke-accordion-content-height` (and `--loke-accordion-content-width`). Use these in CSS — never use `max-height` with a fixed value.

```css
.accordion-content {
  overflow: hidden;
}

.accordion-content[data-state="open"] {
  animation: accordion-open 200ms ease-out;
}

.accordion-content[data-state="closed"] {
  animation: accordion-close 200ms ease-in;
}

@keyframes accordion-open {
  from { height: 0; }
  to   { height: var(--loke-accordion-content-height); }
}

@keyframes accordion-close {
  from { height: var(--loke-accordion-content-height); }
  to   { height: 0; }
}
```

`data-state` is `"open"` or `"closed"` on `AccordionItem`, `AccordionHeader`, `AccordionTrigger`, and `AccordionContent`.

### Horizontal orientation

Flips keyboard navigation to use ArrowLeft/ArrowRight instead of ArrowUp/ArrowDown.

```tsx
<Accordion type="single" collapsible orientation="horizontal">
  <AccordionItem value="tab-1">
    <AccordionHeader>
      <AccordionTrigger>Panel 1</AccordionTrigger>
    </AccordionHeader>
    <AccordionContent>Content 1</AccordionContent>
  </AccordionItem>
</Accordion>
```

### Controlled state

```tsx
const [value, setValue] = useState("");

<Accordion type="single" collapsible value={value} onValueChange={setValue}>
  <AccordionItem value="a">
    <AccordionHeader>
      <AccordionTrigger>Item A</AccordionTrigger>
    </AccordionHeader>
    <AccordionContent>Content A</AccordionContent>
  </AccordionItem>
</Accordion>
```

For `type="multiple"`, `value` is `string[]` and `onValueChange` receives `string[]`.

## Common Mistakes

### Missing `type` prop — TypeScript error and undefined runtime behavior

`Accordion` is a discriminated union on `type`. Omitting it is a compile error and will not render correctly.

```tsx
// Wrong — type is required
<Accordion>
  <AccordionItem value="a">...</AccordionItem>
</Accordion>

// Correct
<Accordion type="single" collapsible>
  <AccordionItem value="a">...</AccordionItem>
</Accordion>
```

Source: `src/components/accordion/accordion.tsx` — type discriminator on `AccordionSingleProps | AccordionMultipleProps`.

### `collapsible` with `type="multiple"` — silently ignored

`collapsible` is only on `AccordionImplSingleProps`. Passing it to a `type="multiple"` accordion has no effect — multiple items are always independently collapsible.

```tsx
// collapsible prop does nothing here
<Accordion type="multiple" collapsible>...</Accordion>
```

Source: `src/components/accordion/accordion.tsx` — `collapsible` only on `AccordionImplSingle`.

### Missing `value` on `AccordionItem` — open/close state breaks silently

Each `AccordionItem` needs a unique string value. Without it, the accordion cannot track which item is open.

```tsx
// Wrong
<AccordionItem>...</AccordionItem>

// Correct
<AccordionItem value="unique-key">...</AccordionItem>
```

Source: `src/components/accordion/accordion.tsx` — `valueContext.value.includes(value)` check.

### Animating with `max-height` instead of CSS variables — janky transitions

The component measures actual content height and provides it as a CSS variable. Hardcoded `max-height` causes jumpy easing because the value does not match actual height.

```css
/* Wrong */
.accordion-content[data-state="open"] {
  max-height: 500px;
}

/* Correct */
.accordion-content[data-state="open"] {
  height: var(--loke-accordion-content-height);
}
```

Source: `src/components/collapsible/collapsible.tsx` — `--loke-collapsible-content-height` dimension measurement, proxied by `AccordionContent`.

## Cross-references

- **Collapsible** (`@loke/ui/collapsible`) — use for a single expandable section; Accordion wraps Collapsible internally
- **Choosing the Right Component** — Accordion vs Collapsible decision guidance
- **Tabs** (`@loke/ui/tabs`) — for switching between panels where only one panel is ever visible
