@loke/ui
Version:
208 lines (159 loc) • 6.07 kB
Markdown
---
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 /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;
}
accordion-open {
from { height: 0; }
to { height: var(--loke-accordion-content-height); }
}
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** (`/ui/collapsible`) — use for a single expandable section; Accordion wraps Collapsible internally
- **Choosing the Right Component** — Accordion vs Collapsible decision guidance
- **Tabs** (`/ui/tabs`) — for switching between panels where only one panel is ever visible