# Accordion (/ui/components/react/accordion)



<Playground component="accordion" />

## Usage [#usage]

```tsx
import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from '@appica/ui-react/accordion'
```

```tsx
<Accordion defaultValue={['shipping']}>
  <AccordionItem value="shipping">
    <AccordionTrigger>How long does shipping take?</AccordionTrigger>
    <AccordionContent>Orders arrive in 3–5 days with standard delivery.</AccordionContent>
  </AccordionItem>
  <AccordionItem value="returns">
    <AccordionTrigger>What is your return policy?</AccordionTrigger>
    <AccordionContent>Return any unused item within 30 days for a full refund.</AccordionContent>
  </AccordionItem>
</Accordion>
```

`Accordion` stacks collapsible sections — ideal for FAQs, settings groups, and any content you want to keep scannable until it's needed. Compose it from four parts, all under `@appica/ui-react/accordion`:

* **`Accordion`** — the root. Owns which items are open (`value`/`defaultValue`), whether `multiple` can be open at once, and the shared `variant` and `icon` styling.
* **`AccordionItem`** — one section, identified by its `value`. Can be individually `disabled`.
* **`AccordionTrigger`** — the header button that toggles its panel; renders the open/close icon.
* **`AccordionContent`** — the collapsible panel, which animates its height open and closed.

The `variant`, `icon`, `iconVariant`, and `iconPosition` props set on the root cascade to every item, and any item or trigger can override them locally.

## Examples [#examples]

### Default [#default]

By default only one panel is open at a time — opening another closes the last. Set the initially open item(s) with `defaultValue`.

```tsx
import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from '@appica/ui-react/accordion'

export default function AccordionDefault() {
  return (
    <Accordion className="max-w-110" defaultValue={['shipping']}>
      <AccordionItem value="shipping">
        <AccordionTrigger>How long does shipping take?</AccordionTrigger>
        <AccordionContent>
          Orders ship within one business day and arrive in 3–5 days with standard delivery.
        </AccordionContent>
      </AccordionItem>
      <AccordionItem value="returns">
        <AccordionTrigger>What is your return policy?</AccordionTrigger>
        <AccordionContent>
          Return any unused item within 30 days for a full refund — no questions asked.
        </AccordionContent>
      </AccordionItem>
      <AccordionItem value="support">
        <AccordionTrigger>How do I contact support?</AccordionTrigger>
        <AccordionContent>
          Reach our team any time at support@example.com; we reply within a few hours.
        </AccordionContent>
      </AccordionItem>
    </Accordion>
  )
}
```

### Variants [#variants]

Three `variant`s change the surface: `default` is a filled card, `alt` is a flat card that gains a border on hover/open, and `flush` drops the card entirely for a borderless list.

```tsx
import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from '@appica/ui-react/accordion'

const variants = ['default', 'alt', 'flush'] as const

export default function AccordionVariants() {
  return (
    <div className="grid w-full max-w-110 gap-6">
      {variants.map((variant) => (
        <div key={variant}>
          <p className="text-foreground-muted mb-2 text-xs font-medium tracking-wide uppercase">{variant}</p>
          <Accordion variant={variant} defaultValue={['one']}>
            <AccordionItem value="one">
              <AccordionTrigger>First section</AccordionTrigger>
              <AccordionContent>The {variant} variant changes the surface, borders, and spacing.</AccordionContent>
            </AccordionItem>
            <AccordionItem value="two">
              <AccordionTrigger>Second section</AccordionTrigger>
              <AccordionContent>Each item still animates its panel height open and closed.</AccordionContent>
            </AccordionItem>
          </Accordion>
        </div>
      ))}
    </div>
  )
}
```

### Icons [#icons]

Switch the indicator with `icon="plus"` (a plus that morphs to a minus) or hide it with `icon={false}`. Wrap it in a tile with `iconVariant="icon-box"`, and move it ahead of the label with `iconPosition="start"`.

```tsx
import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from '@appica/ui-react/accordion'

export default function AccordionIcons() {
  return (
    <div className="grid w-full max-w-110 gap-6">
      <Accordion icon="plus" defaultValue={['a']}>
        <AccordionItem value="a">
          <AccordionTrigger>Plus icon</AccordionTrigger>
          <AccordionContent>The plus morphs into a minus as the panel opens.</AccordionContent>
        </AccordionItem>
        <AccordionItem value="b">
          <AccordionTrigger>Second item</AccordionTrigger>
          <AccordionContent>Set icon="plus" on the Accordion to apply it to every trigger.</AccordionContent>
        </AccordionItem>
      </Accordion>

      <Accordion icon="chevron" iconVariant="icon-box" iconPosition="start" defaultValue={['c']}>
        <AccordionItem value="c">
          <AccordionTrigger>Boxed icon, leading</AccordionTrigger>
          <AccordionContent>
            iconVariant="icon-box" wraps the icon in a tile; iconPosition="start" moves it ahead of the label.
          </AccordionContent>
        </AccordionItem>
        <AccordionItem value="d">
          <AccordionTrigger>Second item</AccordionTrigger>
          <AccordionContent>The boxed icon sits in a bordered tile that matches the item's surface.</AccordionContent>
        </AccordionItem>
      </Accordion>
    </div>
  )
}
```

### Open multiple [#open-multiple]

Set `multiple` to let any number of panels stay open at once — useful when sections are independent rather than mutually exclusive.

```tsx
import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from '@appica/ui-react/accordion'

export default function AccordionMultiple() {
  return (
    <Accordion className="max-w-110" multiple defaultValue={['plan', 'billing']}>
      <AccordionItem value="plan">
        <AccordionTrigger>Plan</AccordionTrigger>
        <AccordionContent>You're on the Pro plan, renewing on the 1st of each month.</AccordionContent>
      </AccordionItem>
      <AccordionItem value="billing">
        <AccordionTrigger>Billing</AccordionTrigger>
        <AccordionContent>Invoices are emailed to your account owner and available in Settings.</AccordionContent>
      </AccordionItem>
      <AccordionItem value="team">
        <AccordionTrigger>Team</AccordionTrigger>
        <AccordionContent>Invite up to 10 teammates on your current plan.</AccordionContent>
      </AccordionItem>
    </Accordion>
  )
}
```

### Disabled item [#disabled-item]

Add `disabled` to an `AccordionItem` to lock it shut. It dims, can't be toggled, and is skipped by keyboard navigation.

```tsx
import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from '@appica/ui-react/accordion'

export default function AccordionDisabled() {
  return (
    <Accordion className="max-w-110" defaultValue={['available']}>
      <AccordionItem value="available">
        <AccordionTrigger>Available section</AccordionTrigger>
        <AccordionContent>This item opens and closes as usual.</AccordionContent>
      </AccordionItem>
      <AccordionItem value="locked" disabled>
        <AccordionTrigger>Locked section</AccordionTrigger>
        <AccordionContent>A disabled item can't be opened and is skipped by keyboard navigation.</AccordionContent>
      </AccordionItem>
      <AccordionItem value="another">
        <AccordionTrigger>Another section</AccordionTrigger>
        <AccordionContent>Focus moves straight here, past the locked item above.</AccordionContent>
      </AccordionItem>
    </Accordion>
  )
}
```

### Leading media [#leading-media]

The trigger label is just `children`, so you can prefix it with any media — a plain icon, a [`Thumbnail`](/ui/components/react/thumbnail), or an [`Avatar`](/ui/components/react/avatar). They line up with the text while the indicator icon stays pinned to the trailing edge.

```tsx
import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from '@appica/ui-react/accordion'
import { Avatar, AvatarImage, AvatarFallback } from '@appica/ui-react/avatar'
import { Thumbnail } from '@appica/ui-react/thumbnail'
import { GBRounded } from '@appica/country-flags-react'
import { Bell, CreditCard } from '@appica/icons-react'

export default function AccordionMixedMedia() {
  return (
    <Accordion className="max-w-110">
      <AccordionItem value="notifications">
        <AccordionTrigger>
          <Bell />
          Notifications
        </AccordionTrigger>
        <AccordionContent>Choose which emails and push alerts you'd like to receive.</AccordionContent>
      </AccordionItem>
      <AccordionItem value="billing">
        <AccordionTrigger>
          <Thumbnail variant="icon-outline" size="sm" shape="rounded" className="-my-1">
            <CreditCard />
          </Thumbnail>
          Billing
        </AccordionTrigger>
        <AccordionContent>Manage your payment method and download past invoices.</AccordionContent>
      </AccordionItem>
      <AccordionItem value="region">
        <AccordionTrigger>
          <GBRounded className="size-6" aria-hidden />
          Region &amp; language
        </AccordionTrigger>
        <AccordionContent>
          Set your country, preferred language, and the currency shown across the app.
        </AccordionContent>
      </AccordionItem>
      <AccordionItem value="account">
        <AccordionTrigger>
          <Avatar size="xs">
            <AvatarImage src="/avatars/01.jpg" alt="Sarah Jenkins" />
            <AvatarFallback>SJ</AvatarFallback>
          </Avatar>
          Account
        </AccordionTrigger>
        <AccordionContent>Update your profile details, email address, and password.</AccordionContent>
      </AccordionItem>
    </Accordion>
  )
}
```

### Controlled [#controlled]

Drive the open items yourself with `value` + `onValueChange`. Here the open panel is read out and stepped through with external prev/next buttons, while clicking a header still updates the same state.

```tsx
'use client'

import { useState } from 'react'
import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from '@appica/ui-react/accordion'
import { Button } from '@appica/ui-react/button'
import { ChevronUp, ChevronDown } from '@appica/icons-react'

const items = [
  {
    value: 'install',
    title: 'Install dependencies',
    body: 'Restore the lockfile and pull the package cache so every build starts from the same baseline.',
  },
  {
    value: 'test',
    title: 'Run the test suite',
    body: 'Unit and integration tests run in parallel; a single failure stops the pipeline before deploy.',
  },
  {
    value: 'deploy',
    title: 'Deploy to production',
    body: 'A green build promotes the artifact behind a canary, then rolls out to the rest of the fleet.',
  },
]

export default function AccordionControlled() {
  const [value, setValue] = useState(['test'])
  const index = items.findIndex((item) => item.value === value[0])

  function step(delta: number) {
    const next = items[Math.min(items.length - 1, Math.max(0, index + delta))]
    if (next) setValue([next.value])
  }

  return (
    <div className="flex w-full max-w-110 flex-col gap-3">
      <div className="flex items-center justify-between">
        <p className="text-foreground-muted text-sm">
          Expanded: <span className="text-foreground-intense font-medium">{value[0] ?? 'none'}</span>
        </p>
        <div className="flex gap-1">
          <Button
            variant="outline"
            size="icon-sm"
            disabled={index <= 0}
            onClick={() => step(-1)}
            aria-label="Previous section"
          >
            <ChevronUp />
          </Button>
          <Button
            variant="outline"
            size="icon-sm"
            disabled={index >= items.length - 1}
            onClick={() => step(1)}
            aria-label="Next section"
          >
            <ChevronDown />
          </Button>
        </div>
      </div>
      <Accordion icon={false} value={value} onValueChange={(next) => setValue(next as string[])}>
        {items.map((item) => (
          <AccordionItem key={item.value} value={item.value}>
            <AccordionTrigger>{item.title}</AccordionTrigger>
            <AccordionContent>{item.body}</AccordionContent>
          </AccordionItem>
        ))}
      </Accordion>
    </div>
  )
}
```

## RTL [#rtl]

Every Appica UI component supports right-to-left layouts out of the box. Set the `dir` attribute on a container (commonly your `<html>` element) so CSS logical properties resolve correctly, and wrap your tree in `DirectionProvider` so direction-aware behavior follows the same direction.

```tsx
import { DirectionProvider } from '@appica/ui-react/providers/direction-provider'

export default function RootLayout({ children }) {
  return (
    <html lang="ar" dir="rtl">
      <body>
        <DirectionProvider dir="rtl">{children}</DirectionProvider>
      </body>
    </html>
  )
}
```

The trigger text aligns to the right and the indicator icon moves to the left, all from CSS logical properties. For setup details and caveats, see the [RTL guide](/ui/docs/react/rtl).

<RtlPreview component="accordion" />

## API reference [#api-reference]

`Accordion` wraps [Base UI's Accordion](https://base-ui.com/react/components/accordion). Each part forwards every remaining prop to the matching Base UI primitive, so anything it accepts works here — the tables below list the Appica additions and the most common props. The panel exposes the `--accordion-panel-height` CSS variable that powers the height animation. See the Base UI docs for the complete API.

### Accordion [#accordion]

The root. Renders a `<div>` and owns the open state and shared styling.

| Prop            | <ColMinWidth width="210">Type</ColMinWidth>      | Default     | <ColMinWidth width="220">Description</ColMinWidth>              |
| --------------- | ------------------------------------------------ | ----------- | --------------------------------------------------------------- |
| `variant`       | <Code>'default' \| 'alt' \| 'flush'</Code>       | `'default'` | Surface style applied to every item.                            |
| `icon`          | <Code>'chevron' \| 'plus' \| false</Code>        | `'chevron'` | The open/close indicator, or `false` to hide it.                |
| `iconVariant`   | <Code>'icon' \| 'icon-box'</Code>                | `'icon'`    | Plain glyph, or wrapped in a tinted tile.                       |
| `iconPosition`  | <Code>'end' \| 'start'</Code>                    | `'end'`     | Place the icon after or before the trigger label.               |
| `value`         | `string[]`                                       | —           | Controlled list of open item values. Pair with `onValueChange`. |
| `defaultValue`  | `string[]`                                       | —           | Uncontrolled initially open item(s).                            |
| `onValueChange` | <Code>(value: string\[], details) => void</Code> | —           | Fires when the set of open items changes.                       |
| `multiple`      | `boolean`                                        | `false`     | Allow several panels to be open simultaneously.                 |
| `disabled`      | `boolean`                                        | `false`     | Disable every item in the accordion.                            |
| `keepMounted`   | `boolean`                                        | `false`     | Keep closed panels in the DOM (e.g. for in-page search / SEO).  |
| `className`     | <Code>string \| ((state) => string)</Code>       | —           | Extra classes, merged via `tailwind-merge`.                     |

### AccordionItem [#accordionitem]

One section. Renders a `<div>` and exposes `data-open` / `data-disabled` for styling.

| Prop           | <ColMinWidth width="210">Type</ColMinWidth>   | Default | <ColMinWidth width="220">Description</ColMinWidth>                        |
| -------------- | --------------------------------------------- | ------- | ------------------------------------------------------------------------- |
| `value`        | `string`                                      | —       | Identifier reported in `value`/`onValueChange`. Auto-assigned if omitted. |
| `variant`      | <Code>'default' \| 'alt' \| 'flush'</Code>    | —       | Override the root's `variant` for this item.                              |
| `disabled`     | `boolean`                                     | `false` | Lock this item shut and skip it during keyboard navigation.               |
| `onOpenChange` | <Code>(open: boolean, details) => void</Code> | —       | Fires when this item opens or closes.                                     |
| `className`    | <Code>string \| ((state) => string)</Code>    | —       | Extra classes, merged via `tailwind-merge`.                               |

### AccordionTrigger [#accordiontrigger]

The header button. Rendered inside an `Accordion.Header`; toggles its panel and shows the icon.

| Prop           | <ColMinWidth width="210">Type</ColMinWidth> | Default | <ColMinWidth width="220">Description</ColMinWidth> |
| -------------- | ------------------------------------------- | ------- | -------------------------------------------------- |
| `icon`         | <Code>'chevron' \| 'plus' \| false</Code>   | —       | Override the root's icon for this trigger.         |
| `iconVariant`  | <Code>'icon' \| 'icon-box'</Code>           | —       | Override the root's icon style.                    |
| `iconPosition` | <Code>'end' \| 'start'</Code>               | —       | Override the root's icon position.                 |
| `children`     | `ReactNode`                                 | —       | The header label.                                  |
| `className`    | <Code>string \| ((state) => string)</Code>  | —       | Extra classes, merged via `tailwind-merge`.        |

### AccordionContent [#accordioncontent]

The collapsible panel. Renders a `<div>` that animates its height between open and closed.

| Prop          | <ColMinWidth width="210">Type</ColMinWidth> | Default | <ColMinWidth width="220">Description</ColMinWidth> |
| ------------- | ------------------------------------------- | ------- | -------------------------------------------------- |
| `children`    | `ReactNode`                                 | —       | The panel content.                                 |
| `keepMounted` | `boolean`                                   | `false` | Keep the panel mounted while closed.               |
| `className`   | <Code>string \| ((state) => string)</Code>  | —       | Extra classes, merged via `tailwind-merge`.        |

## Accessibility [#accessibility]

* Each `AccordionTrigger` is a real `<button>` inside a heading, with `aria-expanded` and `aria-controls` wired to its panel — assistive tech announces the open/closed state.
* Keyboard support follows the WAI-ARIA accordion pattern: **Tab** moves between triggers, **Enter**/**Space** toggles the focused panel, and the **arrow keys** move focus between headers (wrapping at the ends).
* A `disabled` item is removed from the tab order and skipped by arrow navigation, and exposes `data-disabled` for styling.
* The indicator icon is decorative (`aria-hidden`); the trigger's text label carries the meaning.
* The panel's height animation honours `prefers-reduced-motion`.
