# Accordion (Caseroom component)

Sections on native details, one open at a time if you like. Keyboard and find-in-page, no JavaScript. Black and white, dark mode, shadcn-compatible; React, Astro and plain HTML.

29% of the 2,062 sites in The UI Index have a accordion (590 sites).

## Install

- React: `npx shadcn@latest add https://www.themes.dev/caseroom/r/accordion.json`
- Astro: `npx shadcn@latest add https://www.themes.dev/caseroom/r/astro/accordion.json`
- HTML: https://www.themes.dev/caseroom/r/html/accordion.html (with https://www.themes.dev/caseroom/r/html/caseroom.css)

Page: https://www.themes.dev/caseroom/components/accordion/ · Rules for agents: https://www.themes.dev/caseroom/AGENT.md

## Code

### accordion.astro

```astro
---
import { cn } from '@/lib/utils';
import { accordion, CHEVRON_PATH, type AccordionItem } from './accordion.variants';

interface Props {
  items: AccordionItem[];
  /** One open at a time: the items share this name (native exclusive details). */
  single?: string;
  class?: string;
}

const { items, single, class: className } = Astro.props;
---

<div class={cn(accordion.root, className)}>
  {
    items.map((it) => (
      <details name={single} open={it.open} class={accordion.item}>
        <summary class={accordion.summary}>
          {it.title}
          <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class={accordion.chevron}><path d={CHEVRON_PATH} /></svg>
        </summary>
        <p class={accordion.content}>{it.content}</p>
      </details>
    ))
  }
</div>
```

### accordion.tsx

```tsx
import { cn } from '@/lib/utils';
import { accordion, CHEVRON_PATH, type AccordionItem } from './accordion.variants';

export type { AccordionItem };

export interface AccordionProps {
  items: AccordionItem[];
  /** One open at a time: the items share this name (native exclusive details). */
  single?: string;
  className?: string;
}

/** Sections on native <details>; the browser handles keyboard, find-in-page and the exclusive group. */
export function Accordion({ items, single, className }: AccordionProps) {
  return (
    <div className={cn(accordion.root, className)}>
      {items.map((it, k) => (
        <details key={k} name={single} open={it.open} className={accordion.item}>
          <summary className={accordion.summary}>
            {it.title}
            <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className={accordion.chevron}><path d={CHEVRON_PATH} /></svg>
          </summary>
          <p className={accordion.content}>{it.content}</p>
        </details>
      ))}
    </div>
  );
}
```

### accordion.variants.ts

```tsx
/** Sections that open and close, on native <details>: keyboard, find-in-page and no JavaScript. With `single`, the
 *  items share a name, so opening one closes the others (the browser does it). Items sit on their own grounds,
 *  separated by space, not by rules; the chevron turns, and stands still under reduced motion. */
export const accordion = {
  root: 'grid w-full gap-2',
  item: 'group rounded-lg bg-muted/60 transition-colors open:bg-muted',
  summary:
    'flex min-h-12 cursor-pointer list-none items-center justify-between gap-4 rounded-lg px-4 py-3 text-sm font-medium text-pretty transition-colors hover:bg-accent active:bg-pressed group-open:rounded-b-none [&::-webkit-details-marker]:hidden',
  chevron: 'size-4 shrink-0 text-muted-foreground transition-transform duration-200 group-open:rotate-180',
  content: 'px-4 pt-1 pb-4 text-sm text-pretty text-muted-foreground',
};

export type AccordionItem = { title: string; content: string; open?: boolean };
export const CHEVRON_PATH = 'M4 6l4 4 4-4';
```
