# Accordion

A vertically stacked set of interactive headings that each reveal a section of content.

```tsx
import * as stylex from "@stylexjs/stylex"
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"

const styles = stylex.create({
  accordion: { maxWidth: "32rem" },
})

export default function AccordionDemo() {
  return (
    <Accordion defaultValue={["shipping"]} xstyle={styles.accordion}>
      <AccordionItem value="shipping">
        <AccordionTrigger>What are your shipping options?</AccordionTrigger>
        <AccordionContent>
          We offer standard (5-7 days), express (2-3 days), and overnight shipping. Free shipping on
          international orders.
        </AccordionContent>
      </AccordionItem>
      <AccordionItem value="returns">
        <AccordionTrigger>What is your return policy?</AccordionTrigger>
        <AccordionContent>
          Returns accepted within 30 days. Items must be unused and in original packaging. Refunds
          processed within 5-7 business days.
        </AccordionContent>
      </AccordionItem>
      <AccordionItem value="support">
        <AccordionTrigger>How can I contact customer support?</AccordionTrigger>
        <AccordionContent>
          Reach us via email, live chat, or phone. We respond within 24 hours during business days.
        </AccordionContent>
      </AccordionItem>
    </Accordion>
  )
}
```

## Installation

1. Install the following dependencies:

   ```bash
   npm install @base-ui/react @stylexjs/stylex clsx
   ```

2. Set up the token layer and the StyleX compiler, if you have not already. See
   [Installation](https://carbon.style/docs/installation), and paste `states.ts` and `state-styles.ts`
   from the same page. The trigger takes its focus ring and its disabled dim from
   the first and reads Base UI's state through the second. The chevron reads the
   trigger's hover and open state through the `accordionTrigger` marker in
   `markers.stylex.ts`, also on that page. `interop.css` sizes any icon you put in
   a trigger.

3. Copy and paste the following code into your project.

   ```tsx title="components/ui/accordion.tsx"
   "use client"

   import type { CSSProperties } from "react"
   import { Accordion as AccordionPrimitive } from "@base-ui/react/accordion"
   import * as stylex from "@stylexjs/stylex"
   import type { StyleXStyles } from "@stylexjs/stylex"
   import { clsx } from "clsx"

   import { stateStyles } from "@/lib/state-styles"
   import { accordionTrigger } from "@/theme/markers.stylex"
   import { states } from "@/theme/states"
   import { color, duration, ease, leading, radius, space, text } from "@/theme/tokens.stylex"

   const styles = stylex.create({
     root: { display: "flex", flexDirection: "column", width: "100%" },
     item: {
       borderBlockEndWidth: { default: 1, ":last-child": 0 },
       borderBlockEndStyle: "solid",
     },
     header: { display: "flex" },
     trigger: {
       display: "flex",
       flexGrow: 1,
       alignItems: "flex-start",
       gap: space.s2,
       textAlign: "start",
       paddingBlock: space.s2_5,
       fontSize: text.sm,
       lineHeight: leading.sm,
       fontWeight: 500,
       borderRadius: radius.lg,
       borderWidth: 1,
       borderStyle: "solid",
       transitionProperty: "border-color, box-shadow",
       transitionDuration: duration.color,
       transitionTimingFunction: ease.standard,
       "--carbon-icon-size": space.s4,
     },
     chevron: {
       marginInlineStart: "auto",
       width: space.s5,
       height: space.s5,
       color: {
         default: color.mutedForeground,
         [stylex.when.ancestor(":hover", accordionTrigger)]: color.foreground,
       },
       transitionProperty: "color",
       transitionDuration: duration.color,
       transitionTimingFunction: ease.standard,
     },
     chevronPath: {
       transformBox: "fill-box",
       transformOrigin: "center",
       transform: {
         default: "none",
         [stylex.when.ancestor("[data-panel-open]", accordionTrigger)]: "scaleY(-1)",
       },
       transitionProperty: "transform",
       transitionDuration: { default: duration.move, "@media (prefers-reduced-motion: reduce)": "0s" },
       transitionTimingFunction: ease.standard,
     },
     content: {
       overflow: "hidden",
       height: "var(--accordion-panel-height)",
       fontSize: text.sm,
       lineHeight: leading.sm,
       transitionProperty: "height",
       transitionDuration: {
         default: duration.panel,
         "@media (prefers-reduced-motion: reduce)": "0s",
       },
       transitionTimingFunction: ease.spring,
     },
     contentHidden: { height: 0 },
     contentInner: { paddingBlockEnd: space.s2_5 },
   })

   type Overrides = { xstyle?: StyleXStyles; className?: string }

   type AccordionProps = Omit<AccordionPrimitive.Root.Props, "className"> & Overrides

   function Accordion({ xstyle, className, ...props }: AccordionProps) {
     const sx = stylex.props(styles.root, xstyle)
     return (
       <AccordionPrimitive.Root
         data-slot="accordion"
         {...sx}
         {...props}
         className={clsx(sx.className, className)}
       />
     )
   }

   type AccordionItemProps = Omit<AccordionPrimitive.Item.Props, "className"> & Overrides

   function AccordionItem({ xstyle, className, ...props }: AccordionItemProps) {
     const sx = stylex.props(styles.item, xstyle)
     return (
       <AccordionPrimitive.Item
         data-slot="accordion-item"
         {...sx}
         {...props}
         className={clsx(sx.className, className)}
       />
     )
   }

   type AccordionTriggerProps = Omit<AccordionPrimitive.Trigger.Props, "className" | "style"> &
     Overrides & {
       style?: CSSProperties
       headerProps?: Omit<AccordionPrimitive.Header.Props, "className">
     }

   function AccordionTrigger({
     xstyle,
     style,
     className,
     headerProps,
     children,
     ...props
   }: AccordionTriggerProps) {
     return (
       <AccordionPrimitive.Header
         data-slot="accordion-header"
         {...stylex.props(styles.header)}
         {...headerProps}
       >
         <AccordionPrimitive.Trigger
           data-slot="accordion-trigger"
           {...props}
           {...stateStyles(
             (state) => [
               accordionTrigger,
               styles.trigger,
               states.focusRing,
               state.disabled && states.disabled,
               xstyle,
             ],
             { className, style, hook: "carbon-icons" },
           )}
         >
           {children}
           <svg
             data-slot="accordion-trigger-icon"
             aria-hidden="true"
             viewBox="0 0 16 16"
             fill="none"
             {...stylex.props(styles.chevron)}
           >
             <path
               d="M4.75 6.75 8 10.25l3.25-3.5"
               stroke="currentColor"
               strokeWidth={1.5}
               strokeLinecap="round"
               strokeLinejoin="round"
               vectorEffect="non-scaling-stroke"
               {...stylex.props(styles.chevronPath)}
             />
           </svg>
         </AccordionPrimitive.Trigger>
       </AccordionPrimitive.Header>
     )
   }

   type AccordionContentProps = Omit<AccordionPrimitive.Panel.Props, "className" | "style"> &
     Overrides & { style?: CSSProperties }

   function AccordionContent({ xstyle, style, className, children, ...props }: AccordionContentProps) {
     const sx = stylex.props(styles.contentInner, xstyle)
     return (
       <AccordionPrimitive.Panel
         data-slot="accordion-content"
         {...props}
         {...stateStyles(
           (state) => [
             styles.content,
             (state.transitionStatus === "starting" || state.transitionStatus === "ending") &&
               styles.contentHidden,
           ],
           {
             style,
           },
         )}
       >
         <div data-slot="accordion-content-inner" {...sx} className={clsx(sx.className, className)}>
           {children}
         </div>
       </AccordionPrimitive.Panel>
     )
   }

   export { Accordion, AccordionContent, AccordionItem, AccordionTrigger }
   ```

4. Update the import paths to match your project setup.

## Usage

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

```tsx
<Accordion defaultValue={["item-1"]}>
  <AccordionItem value="item-1">
    <AccordionTrigger>Is it accessible?</AccordionTrigger>
    <AccordionContent>Yes. It follows the WAI-ARIA disclosure pattern.</AccordionContent>
  </AccordionItem>
</Accordion>
```

`value` and `defaultValue` are arrays, one entry per open item, even when only
one item can be open at a time.

## Composition

```text
Accordion
├── AccordionItem
│   ├── AccordionTrigger
│   └── AccordionContent
└── AccordionItem
    ├── AccordionTrigger
    └── AccordionContent
```

`AccordionTrigger` renders the Base UI `Header` and `Trigger` together, and
`AccordionContent` renders the `Panel` around an inner element that holds the
padding.

## Multiple

Use the `multiple` prop to keep more than one item open at a time.

```tsx
import * as stylex from "@stylexjs/stylex"
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"

const styles = stylex.create({
  accordion: { maxWidth: "32rem" },
})

const items = [
  {
    value: "notifications",
    trigger: "Notification Settings",
    content:
      "Manage how you receive notifications. You can enable email alerts for updates or push notifications for mobile devices.",
  },
  {
    value: "privacy",
    trigger: "Privacy & Security",
    content:
      "Control your privacy settings and security preferences. Enable two-factor authentication, manage connected devices, and review active sessions.",
  },
  {
    value: "billing",
    trigger: "Billing & Subscription",
    content:
      "View your current plan, payment history, and upcoming invoices. Update your payment method or cancel your subscription.",
  },
]

export default function AccordionMultiple() {
  return (
    <Accordion multiple defaultValue={["notifications", "privacy"]} xstyle={styles.accordion}>
      {items.map((item) => (
        <AccordionItem key={item.value} value={item.value}>
          <AccordionTrigger>{item.trigger}</AccordionTrigger>
          <AccordionContent>{item.content}</AccordionContent>
        </AccordionItem>
      ))}
    </Accordion>
  )
}
```

## Disabled

Use the `disabled` prop on `AccordionItem` to disable a single item, or on
`Accordion` to disable all of them.

```tsx
import * as stylex from "@stylexjs/stylex"
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"

const styles = stylex.create({
  accordion: { maxWidth: "32rem" },
})

export default function AccordionDisabled() {
  return (
    <Accordion xstyle={styles.accordion}>
      <AccordionItem value="history">
        <AccordionTrigger>Can I access my account history?</AccordionTrigger>
        <AccordionContent>
          Yes. Your transactions, plan changes, and support tickets are in the Account History
          section of your dashboard.
        </AccordionContent>
      </AccordionItem>
      <AccordionItem value="premium" disabled>
        <AccordionTrigger>Premium feature information</AccordionTrigger>
        <AccordionContent>
          This section covers premium features. Upgrade your plan to read it.
        </AccordionContent>
      </AccordionItem>
      <AccordionItem value="email">
        <AccordionTrigger>How do I update my email address?</AccordionTrigger>
        <AccordionContent>
          Change it in your account settings. A verification email goes to the new address to
          confirm the change.
        </AccordionContent>
      </AccordionItem>
    </Accordion>
  )
}
```

## Borders

Items draw a rule along their bottom edge, except the last. Add a frame through
`xstyle` on the `Accordion` and inline padding on each `AccordionItem`.

```tsx
import * as stylex from "@stylexjs/stylex"
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"
import { radius, space } from "@/theme/tokens.stylex"

const styles = stylex.create({
  accordion: {
    maxWidth: "32rem",
    borderWidth: 1,
    borderStyle: "solid",
    borderRadius: radius.xl,
  },
  item: { paddingInline: space.s4 },
})

const items = [
  {
    value: "billing",
    trigger: "How does billing work?",
    content:
      "Monthly and annual plans are charged at the start of each cycle. Cancel any time. Every plan includes automatic backups and unlimited team members.",
  },
  {
    value: "security",
    trigger: "Is my data secure?",
    content:
      "We use end-to-end encryption, SOC 2 Type II compliance, and third-party security audits. Data is encrypted at rest and in transit.",
  },
  {
    value: "integration",
    trigger: "What integrations do you support?",
    content:
      "We integrate with 500+ tools including Slack, Zapier, Salesforce, and HubSpot. Build your own with the REST API and webhooks.",
  },
]

export default function AccordionBorders() {
  return (
    <Accordion defaultValue={["billing"]} xstyle={styles.accordion}>
      {items.map((item) => (
        <AccordionItem key={item.value} value={item.value} xstyle={styles.item}>
          <AccordionTrigger>{item.trigger}</AccordionTrigger>
          <AccordionContent>{item.content}</AccordionContent>
        </AccordionItem>
      ))}
    </Accordion>
  )
}
```

## API reference

Pass `render` to replace a part's element or compose it with another component.

Every part wraps the Base UI `Accordion` part of the same name and accepts its
props. See the [Base UI reference](https://base-ui.com/react/components/accordion)
for the full list. Each part also takes `xstyle` and `className`.

### Accordion

| Prop               | Type                                 | Default |
| ------------------ | ------------------------------------ | ------- |
| `value`            | `string[]`                           | —       |
| `defaultValue`     | `string[]`                           | —       |
| `onValueChange`    | `(value: string[], details) => void` | —       |
| `multiple`         | `boolean`                            | `false` |
| `disabled`         | `boolean`                            | `false` |
| `keepMounted`      | `boolean`                            | `false` |
| `hiddenUntilFound` | `boolean`                            | `false` |

`hiddenUntilFound` hides a closed panel with `hidden="until-found"` instead of
unmounting it, which lets the browser's find-in-page reach the text and open
the item. It overrides `keepMounted`.

### AccordionItem

| Prop           | Type                               | Default |
| -------------- | ---------------------------------- | ------- |
| `value`        | `string`                           | —       |
| `disabled`     | `boolean`                          | —       |
| `onOpenChange` | `(open: boolean, details) => void` | —       |

An item with no `value` gets a generated one. Set it when you control the
accordion or want an item open on first render.

### AccordionTrigger

| Prop          | Type                     | Default |
| ------------- | ------------------------ | ------- |
| `headerProps` | `Accordion.Header.Props` | —       |
| `style`       | `CSSProperties`          | —       |

The heading is the primitive's `<h3>`. Pass
`headerProps={{ render: <h2 /> }}` to fit the page's outline.

`style` is real inline CSS, merged over the compiled styles.

### AccordionContent

`xstyle` and `className` apply to the inner element. `style` takes real inline
CSS on the panel. The panel animates its height between `0` and the measured
content height, and a padded box of height `0` still draws its padding.

### Data attributes

| Attribute                                  | On          | Effect                                       |
| ------------------------------------------ | ----------- | -------------------------------------------- |
| `data-slot="accordion-content"`            | the panel   | Targets the animated panel                   |
| `data-starting-style`, `data-ending-style` | the panel   | Base UI writes them while the panel animates |
| `data-open`, `data-disabled`               | item, panel | Base UI writes them                          |
| `data-panel-open`, `data-disabled`         | the trigger | Base UI writes them; the chevron flips       |
| `data-slot="accordion-trigger-icon"`       | the chevron | Targets the chevron from a parent            |

### CSS variables

| Variable                   | On        | Effect                                                          |
| -------------------------- | --------- | --------------------------------------------------------------- |
| `--accordion-panel-height` | the panel | Base UI writes the measured height; the panel transitions to it |
