# Accordion

> Stacked headings that each reveal a panel, with height motion you can reverse mid-way and panels that stay searchable while closed.

Docs: https://hextaui.com/docs/accordion
Markdown: https://hextaui.com/docs/accordion.md

```tsx title="components/examples/accordion/demo.tsx"
import {
  Accordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
} from "@/components/ui/accordion"

const items = [
  {
    value: "what",
    question: "What is HextaUI?",
    answer:
      "A collection of components built on top of shadcn/ui, with careful attention to structure and micro-interactions.",
  },
  {
    value: "install",
    question: "How do I install a component?",
    answer:
      "Copy the source into your project, then edit it like any other file you own.",
  },
  {
    value: "license",
    question: "Can I use it in commercial projects?",
    answer:
      "Yes. Every component is free and open source, for personal and commercial work.",
  },
]

export function AccordionDemo() {
  return (
    <Accordion defaultValue={["what"]} className="w-full max-w-md">
      {items.map((item) => (
        <AccordionItem key={item.value} value={item.value}>
          <AccordionTrigger>{item.question}</AccordionTrigger>
          <AccordionContent>{item.answer}</AccordionContent>
        </AccordionItem>
      ))}
    </Accordion>
  )
}
```

## Installation

### CLI

```bash
npx shadcn@latest add https://hextaui.com/r/accordion.json
```

This adds the component, the HextaUI theme tokens and any HextaUI components it depends on.

### Manual

Add the HextaUI theme tokens (https://hextaui.com/docs/installation#theme) to your global CSS if you haven't yet, then install the dependencies.

```bash
pnpm add @base-ui/react @tabler/icons-react class-variance-authority cn
```

Copy and paste the following code into your project.

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

import * as React from "react"
import { Accordion as AccordionPrimitive } from "@base-ui/react/accordion"
import { IconChevronDown } from "@tabler/icons-react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "cn"

const accordionVariants = cva("flex w-full flex-col", {
  variants: {
    variant: {
      default: "",
      outline:
        "overflow-hidden rounded-lg ring-(length:--hairline) ring-border forced-colors:border [&>[data-slot=accordion-item]:first-child>[data-slot=accordion-header]>[data-slot=accordion-trigger]]:rounded-t-lg [&>[data-slot=accordion-item]:last-child:not([data-open])>[data-slot=accordion-header]>[data-slot=accordion-trigger]]:rounded-b-lg",
      separated: "gap-2",
      ghost: "gap-1",
    },
  },
  defaultVariants: {
    variant: "default",
  },
})

type AccordionVariant = NonNullable<
  VariantProps<typeof accordionVariants>["variant"]
>

const AccordionContext = React.createContext<AccordionVariant>("default")

const accordionItemVariants = cva("", {
  variants: {
    variant: {
      default:
        "relative after:pointer-events-none after:absolute after:inset-x-0 after:bottom-0 after:h-px after:shadow-[inset_0_calc(-1*var(--hairline))_0_var(--color-border)] last:after:hidden",
      outline:
        "relative after:pointer-events-none after:absolute after:inset-x-0 after:bottom-0 after:h-px after:shadow-[inset_0_calc(-1*var(--hairline))_0_var(--color-border)] last:after:hidden",
      separated:
        "rounded-lg inset-ring-(length:--hairline) inset-ring-border transition-[background-color,box-shadow] duration-150 has-[>[data-slot=accordion-header]>[data-slot=accordion-trigger]:focus-visible]:ring-3 has-[>[data-slot=accordion-header]>[data-slot=accordion-trigger]:focus-visible]:ring-focus-ring has-[>[data-slot=accordion-header]>[data-slot=accordion-trigger]:focus-visible]:inset-ring-ring forced-colors:border data-open:bg-muted/40 [@media(hover:hover)]:has-[>[data-slot=accordion-header]>[data-slot=accordion-trigger]:not([data-disabled]):hover]:bg-muted/40",
      ghost: "rounded-md data-open:bg-muted/50",
    },
  },
  defaultVariants: {
    variant: "default",
  },
})

const accordionTriggerVariants = cva(
  "group/accordion-trigger relative flex flex-1 items-start gap-4 text-start text-sm font-medium transition-[color,background-color,box-shadow] duration-150 outline-none select-none [-webkit-tap-highlight-color:transparent] focus-visible:z-10 focus-visible:outline-hidden data-disabled:pointer-events-none data-disabled:opacity-50",
  {
    variants: {
      variant: {
        default:
          "-mx-2 rounded-md px-2 py-4 focus-visible:ring-3 focus-visible:ring-focus-ring focus-visible:outline-1 focus-visible:outline-ring focus-visible:outline-solid",
        outline:
          "px-4 py-4 hover:bg-muted/50 focus-visible:ring-3 focus-visible:ring-focus-ring focus-visible:outline-1 focus-visible:-outline-offset-1 focus-visible:outline-ring focus-visible:outline-solid focus-visible:ring-inset",
        separated: "rounded-lg px-4 py-4",
        ghost:
          "rounded-md px-3 py-3 transition-[color,box-shadow] hover:bg-muted focus-visible:ring-3 focus-visible:ring-focus-ring focus-visible:outline-1 focus-visible:outline-ring focus-visible:outline-solid",
      },
    },
    defaultVariants: {
      variant: "default",
    },
  }
)

const accordionContentVariants = cva(
  "text-sm wrap-anywhere text-muted-foreground [&_a:not([data-slot])]:text-foreground [&_a:not([data-slot])]:underline [&_a:not([data-slot])]:underline-offset-3 [&_a:not([data-slot])]:hover:text-foreground/80 [&>p:not(:last-child)]:mb-3",
  {
    variants: {
      variant: {
        default: "pe-8 pb-4",
        outline: "px-4 pb-4",
        separated: "px-4 pb-4",
        ghost: "px-3 pb-3",
      },
    },
    defaultVariants: {
      variant: "default",
    },
  }
)

type ClassName<State> =
  string | ((state: State) => string | undefined) | undefined

function mergeClassName<State>(base: string, className: ClassName<State>) {
  return typeof className === "function"
    ? (state: State) => cn(base, className(state))
    : cn(base, className)
}

const navigationKeys = ["ArrowDown", "ArrowUp", "Home", "End"]

function focusAdjacentTrigger(event: React.KeyboardEvent<HTMLDivElement>) {
  const root = event.currentTarget
  const current = event.target as HTMLElement

  if (
    !navigationKeys.includes(event.key) ||
    event.altKey ||
    event.ctrlKey ||
    event.metaKey ||
    event.shiftKey ||
    current.dataset.slot !== "accordion-trigger" ||
    current.closest('[data-slot="accordion"]') !== root
  ) {
    return
  }

  const triggers = Array.from(
    root.querySelectorAll<HTMLElement>('[data-slot="accordion-trigger"]')
  ).filter(
    (trigger) =>
      trigger.closest('[data-slot="accordion"]') === root &&
      !trigger.hasAttribute("data-disabled")
  )
  const index = triggers.indexOf(current)
  const last = triggers.length - 1
  const nextIndex = {
    ArrowDown: index === last ? 0 : index + 1,
    ArrowUp: index <= 0 ? last : index - 1,
    Home: 0,
    End: last,
  }[event.key]

  if (nextIndex === undefined || !triggers[nextIndex]) {
    return
  }

  event.preventDefault()
  triggers[nextIndex].focus()
}

type AccordionProps<Value> = AccordionPrimitive.Root.Props<Value> & {
  variant?: AccordionVariant
  hiddenUntilFound?: boolean
}

function Accordion<Value = unknown>({
  className,
  variant = "default",
  hiddenUntilFound = true,
  onKeyDown,
  ...props
}: AccordionProps<Value>) {
  const resolvedVariant = variant ?? "default"

  return (
    <AccordionContext.Provider value={resolvedVariant}>
      <AccordionPrimitive.Root
        data-slot="accordion"
        data-variant={resolvedVariant}
        hiddenUntilFound={hiddenUntilFound}
        onKeyDown={(event) => {
          onKeyDown?.(event)
          if (!event.defaultPrevented) {
            focusAdjacentTrigger(event)
          }
        }}
        className={mergeClassName(
          accordionVariants({ variant: resolvedVariant }),
          className
        )}
        {...props}
      />
    </AccordionContext.Provider>
  )
}

function AccordionItem({ className, ...props }: AccordionPrimitive.Item.Props) {
  const variant = React.useContext(AccordionContext)

  return (
    <AccordionPrimitive.Item
      data-slot="accordion-item"
      className={mergeClassName(accordionItemVariants({ variant }), className)}
      {...props}
    />
  )
}

function AccordionTrigger({
  className,
  children,
  icon,
  ...props
}: AccordionPrimitive.Trigger.Props & {
  icon?: React.ReactNode
}) {
  const variant = React.useContext(AccordionContext)

  return (
    <AccordionPrimitive.Header data-slot="accordion-header" className="flex">
      <AccordionPrimitive.Trigger
        data-slot="accordion-trigger"
        className={mergeClassName(
          accordionTriggerVariants({ variant }),
          className
        )}
        {...props}
      >
        <span
          data-slot="accordion-trigger-label"
          className={cn(
            "min-w-0 flex-1 text-pretty wrap-anywhere [&>svg]:me-2 [&>svg]:inline-block [&>svg]:align-[-0.1875em] [&>svg]:text-muted-foreground [&>svg:not([class*='size-'])]:size-4",
            variant === "default" &&
              "underline decoration-transparent underline-offset-4 transition-[text-decoration-color] duration-150 group-hover/accordion-trigger:decoration-current"
          )}
        >
          {children}
        </span>
        {icon === null ? null : (
          <span
            data-slot="accordion-trigger-icon"
            aria-hidden="true"
            className="ms-auto mt-0.5 flex size-4 shrink-0 items-center justify-center text-muted-foreground transition-[color,translate] duration-150 group-hover/accordion-trigger:text-foreground group-active/accordion-trigger:translate-y-px group-data-panel-open/accordion-trigger:text-foreground motion-reduce:transition-none [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4"
          >
            {icon ?? (
              <IconChevronDown className="transition-[rotate] duration-200 ease-spring group-data-panel-open/accordion-trigger:rotate-180 motion-reduce:transition-none" />
            )}
          </span>
        )}
      </AccordionPrimitive.Trigger>
    </AccordionPrimitive.Header>
  )
}

function useSettledPanel(panelRef: React.RefObject<HTMLDivElement | null>) {
  React.useLayoutEffect(() => {
    const panel = panelRef.current
    if (!panel) {
      return
    }

    const sync = () => {
      const open =
        panel.hasAttribute("data-open") &&
        !panel.hasAttribute("data-starting-style") &&
        !panel.hasAttribute("data-ending-style")
      const animating =
        typeof panel.getAnimations === "function" &&
        panel
          .getAnimations()
          .some(
            (animation) =>
              "transitionProperty" in animation &&
              animation.transitionProperty === "height"
          )
      panel.toggleAttribute("data-settled", open && !animating)
    }

    sync()
    const observer = new MutationObserver(sync)
    observer.observe(panel, {
      attributes: true,
      attributeFilter: [
        "data-open",
        "data-starting-style",
        "data-ending-style",
        "style",
      ],
    })
    panel.addEventListener("transitionend", sync)
    panel.addEventListener("transitioncancel", sync)

    return () => {
      observer.disconnect()
      panel.removeEventListener("transitionend", sync)
      panel.removeEventListener("transitioncancel", sync)
      panel.removeAttribute("data-settled")
    }
  }, [panelRef])
}

function AccordionContent({
  className,
  children,
  ref,
  ...props
}: Omit<AccordionPrimitive.Panel.Props, "className"> & {
  className?: string
}) {
  const variant = React.useContext(AccordionContext)
  const panelRef = React.useRef<HTMLDivElement>(null)
  const setPanelRef = React.useCallback(
    (node: HTMLDivElement | null) => {
      panelRef.current = node
      if (typeof ref === "function") {
        ref(node)
      } else if (ref) {
        ref.current = node
      }
    },
    [ref]
  )
  useSettledPanel(panelRef)

  return (
    <AccordionPrimitive.Panel
      ref={setPanelRef}
      data-slot="accordion-content"
      className="group/accordion-content h-(--accordion-panel-height) overflow-hidden transition-[height] duration-300 ease-spring data-ending-style:h-0 data-ending-style:duration-150 data-settled:overflow-visible data-starting-style:h-0 motion-reduce:transition-none"
      {...props}
    >
      <div
        data-slot="accordion-content-inner"
        className={cn(
          accordionContentVariants({ variant }),
          "transition-[opacity,translate] duration-200 ease-out-quint group-data-ending-style/accordion-content:-translate-y-1 group-data-ending-style/accordion-content:opacity-0 group-data-ending-style/accordion-content:duration-150 group-data-starting-style/accordion-content:-translate-y-1 group-data-starting-style/accordion-content:opacity-0 motion-reduce:transition-none",
          className
        )}
      >
        {children}
      </div>
    </AccordionPrimitive.Panel>
  )
}

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

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 accordion pattern.
    </AccordionContent>
  </AccordionItem>
</Accordion>
```

## Composition

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

## Examples

### Outline

Items share one bordered surface, with the outer corners rounded.

```tsx title="components/examples/accordion/outline.tsx"
import {
  Accordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
} from "@/components/ui/accordion"

const items = [
  {
    value: "what",
    question: "What is HextaUI?",
    answer:
      "A collection of components built on top of shadcn/ui, with careful attention to structure and micro-interactions.",
  },
  {
    value: "install",
    question: "How do I install a component?",
    answer:
      "Copy the source into your project, then edit it like any other file you own.",
  },
  {
    value: "license",
    question: "Can I use it in commercial projects?",
    answer:
      "Yes. Every component is free and open source, for personal and commercial work.",
  },
]

export function AccordionOutline() {
  return (
    <Accordion
      variant="outline"
      defaultValue={["what"]}
      className="w-full max-w-md"
    >
      {items.map((item) => (
        <AccordionItem key={item.value} value={item.value}>
          <AccordionTrigger>{item.question}</AccordionTrigger>
          <AccordionContent>{item.answer}</AccordionContent>
        </AccordionItem>
      ))}
    </Accordion>
  )
}
```

### Separated

Each item is its own card.

```tsx title="components/examples/accordion/separated.tsx"
import {
  Accordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
} from "@/components/ui/accordion"

const items = [
  {
    value: "what",
    question: "What is HextaUI?",
    answer:
      "A collection of components built on top of shadcn/ui, with careful attention to structure and micro-interactions.",
  },
  {
    value: "install",
    question: "How do I install a component?",
    answer:
      "Copy the source into your project, then edit it like any other file you own.",
  },
  {
    value: "license",
    question: "Can I use it in commercial projects?",
    answer:
      "Yes. Every component is free and open source, for personal and commercial work.",
  },
]

export function AccordionSeparated() {
  return (
    <Accordion
      variant="separated"
      defaultValue={["what"]}
      className="w-full max-w-md"
    >
      {items.map((item) => (
        <AccordionItem key={item.value} value={item.value}>
          <AccordionTrigger>{item.question}</AccordionTrigger>
          <AccordionContent>{item.answer}</AccordionContent>
        </AccordionItem>
      ))}
    </Accordion>
  )
}
```

### Ghost

No borders. Triggers get a soft fill on hover and when open.

```tsx title="components/examples/accordion/ghost.tsx"
import {
  Accordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
} from "@/components/ui/accordion"

const items = [
  {
    value: "what",
    question: "What is HextaUI?",
    answer:
      "A collection of components built on top of shadcn/ui, with careful attention to structure and micro-interactions.",
  },
  {
    value: "install",
    question: "How do I install a component?",
    answer:
      "Copy the source into your project, then edit it like any other file you own.",
  },
  {
    value: "license",
    question: "Can I use it in commercial projects?",
    answer:
      "Yes. Every component is free and open source, for personal and commercial work.",
  },
]

export function AccordionGhost() {
  return (
    <Accordion
      variant="ghost"
      defaultValue={["what"]}
      className="w-full max-w-md"
    >
      {items.map((item) => (
        <AccordionItem key={item.value} value={item.value}>
          <AccordionTrigger>{item.question}</AccordionTrigger>
          <AccordionContent>{item.answer}</AccordionContent>
        </AccordionItem>
      ))}
    </Accordion>
  )
}
```

### Multiple

Set `multiple` to let more than one item stay open.

```tsx title="components/examples/accordion/multiple.tsx"
import {
  Accordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
} from "@/components/ui/accordion"

const items = [
  {
    value: "what",
    question: "What is HextaUI?",
    answer:
      "A collection of components built on top of shadcn/ui, with careful attention to structure and micro-interactions.",
  },
  {
    value: "install",
    question: "How do I install a component?",
    answer:
      "Copy the source into your project, then edit it like any other file you own.",
  },
  {
    value: "license",
    question: "Can I use it in commercial projects?",
    answer:
      "Yes. Every component is free and open source, for personal and commercial work.",
  },
]

export function AccordionMultiple() {
  return (
    <Accordion
      variant="outline"
      multiple
      defaultValue={["what", "install"]}
      className="w-full max-w-md"
    >
      {items.map((item) => (
        <AccordionItem key={item.value} value={item.value}>
          <AccordionTrigger>{item.question}</AccordionTrigger>
          <AccordionContent>{item.answer}</AccordionContent>
        </AccordionItem>
      ))}
    </Accordion>
  )
}
```

### Disabled

Disable a single item with `disabled` on `<AccordionItem />`, or the whole accordion on the root.

```tsx title="components/examples/accordion/disabled.tsx"
import {
  Accordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
} from "@/components/ui/accordion"

const items = [
  {
    value: "what",
    question: "What is HextaUI?",
    answer:
      "A collection of components built on top of shadcn/ui, with careful attention to structure and micro-interactions.",
  },
  {
    value: "install",
    question: "How do I install a component?",
    answer:
      "Copy the source into your project, then edit it like any other file you own.",
  },
  {
    value: "license",
    question: "Can I use it in commercial projects?",
    answer:
      "Yes. Every component is free and open source, for personal and commercial work.",
  },
]

export function AccordionDisabled() {
  return (
    <Accordion variant="outline" className="w-full max-w-md">
      {items.map((item) => (
        <AccordionItem
          key={item.value}
          value={item.value}
          disabled={item.value === "install"}
        >
          <AccordionTrigger>{item.question}</AccordionTrigger>
          <AccordionContent>{item.answer}</AccordionContent>
        </AccordionItem>
      ))}
    </Accordion>
  )
}
```

### Controlled

Pass `value` and `onValueChange` to keep the open items in your own state.

```tsx title="components/examples/accordion/controlled.tsx"
"use client"

import * as React from "react"

import {
  Accordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
} from "@/components/ui/accordion"
import { Button } from "@/components/ui/button"

const items = [
  {
    value: "what",
    question: "What is HextaUI?",
    answer:
      "A collection of components built on top of shadcn/ui, with careful attention to structure and micro-interactions.",
  },
  {
    value: "install",
    question: "How do I install a component?",
    answer:
      "Copy the source into your project, then edit it like any other file you own.",
  },
  {
    value: "license",
    question: "Can I use it in commercial projects?",
    answer:
      "Yes. Every component is free and open source, for personal and commercial work.",
  },
]

export function AccordionControlled() {
  const [value, setValue] = React.useState<string[]>(["what"])

  return (
    <div className="flex w-full max-w-md flex-col gap-3">
      <div className="flex gap-2">
        <Button
          variant="outline"
          size="sm"
          onClick={() => setValue(items.map((item) => item.value))}
        >
          Open all
        </Button>
        <Button variant="outline" size="sm" onClick={() => setValue([])}>
          Close all
        </Button>
      </div>
      <Accordion
        variant="outline"
        multiple
        value={value}
        onValueChange={setValue}
      >
        {items.map((item) => (
          <AccordionItem key={item.value} value={item.value}>
            <AccordionTrigger>{item.question}</AccordionTrigger>
            <AccordionContent>{item.answer}</AccordionContent>
          </AccordionItem>
        ))}
      </Accordion>
    </div>
  )
}
```

### Custom icon

Pass any element to `icon`. Style its open state with `group-data-panel-open/accordion-trigger`.

```tsx title="components/examples/accordion/custom-icon.tsx"
import { IconPlus } from "@tabler/icons-react"

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

const items = [
  {
    value: "what",
    question: "What is HextaUI?",
    answer:
      "A collection of components built on top of shadcn/ui, with careful attention to structure and micro-interactions.",
  },
  {
    value: "install",
    question: "How do I install a component?",
    answer:
      "Copy the source into your project, then edit it like any other file you own.",
  },
  {
    value: "license",
    question: "Can I use it in commercial projects?",
    answer:
      "Yes. Every component is free and open source, for personal and commercial work.",
  },
]

export function AccordionCustomIcon() {
  return (
    <Accordion variant="ghost" className="w-full max-w-md">
      {items.map((item) => (
        <AccordionItem key={item.value} value={item.value}>
          <AccordionTrigger
            icon={
              <IconPlus className="transition-transform duration-200 ease-out-cubic group-data-panel-open/accordion-trigger:rotate-45 motion-reduce:transition-none" />
            }
          >
            {item.question}
          </AccordionTrigger>
          <AccordionContent>{item.answer}</AccordionContent>
        </AccordionItem>
      ))}
    </Accordion>
  )
}
```

### Leading icons

Icons before the label are sized and aligned for you. `icon={null}` removes the chevron.

```tsx title="components/examples/accordion/leading-icons.tsx"
import { IconLock, IconUser } from "@tabler/icons-react"

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

export function AccordionLeadingIcons() {
  return (
    <Accordion variant="outline" className="w-full max-w-md">
      <AccordionItem value="account">
        <AccordionTrigger icon={null}>
          <IconUser />
          Account
        </AccordionTrigger>
        <AccordionContent>Name, email and avatar.</AccordionContent>
      </AccordionItem>
      <AccordionItem value="security">
        <AccordionTrigger icon={null}>
          <IconLock />
          Security
        </AccordionTrigger>
        <AccordionContent>Password and two-factor settings.</AccordionContent>
      </AccordionItem>
    </Accordion>
  )
}
```

### Long content

Long titles wrap while the icon stays on the first line. Closed panels stay searchable, so Cmd/Ctrl+F for “retention” opens this one.

```tsx title="components/examples/accordion/long-content.tsx"
import {
  Accordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
} from "@/components/ui/accordion"

export function AccordionLongContent() {
  return (
    <Accordion className="w-full max-w-md">
      <AccordionItem value="long">
        <AccordionTrigger>
          What happens to my data if I cancel my subscription halfway through a
          billing period and later decide to come back?
        </AccordionTrigger>
        <AccordionContent>
          <p>
            Your workspace is kept for 30 days after cancellation. During that
            time you can reactivate and everything is restored exactly as you
            left it.
          </p>
          <p>
            After the retention window, data is permanently deleted. Read the{" "}
            <a href="#">data policy</a> for details.
          </p>
        </AccordionContent>
      </AccordionItem>
    </Accordion>
  )
}
```

### Nested

An inner accordion keeps its own variant, focus styles and arrow-key navigation.

```tsx title="components/examples/accordion/nested.tsx"
import {
  Accordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
} from "@/components/ui/accordion"

const items = [
  {
    value: "what",
    question: "What is HextaUI?",
    answer:
      "A collection of components built on top of shadcn/ui, with careful attention to structure and micro-interactions.",
  },
  {
    value: "install",
    question: "How do I install a component?",
    answer:
      "Copy the source into your project, then edit it like any other file you own.",
  },
  {
    value: "license",
    question: "Can I use it in commercial projects?",
    answer:
      "Yes. Every component is free and open source, for personal and commercial work.",
  },
]

export function AccordionNested() {
  return (
    <Accordion variant="separated" className="w-full max-w-md">
      <AccordionItem value="billing">
        <AccordionTrigger>Billing</AccordionTrigger>
        <AccordionContent>
          <Accordion variant="ghost">
            {items.map((item) => (
              <AccordionItem key={item.value} value={item.value}>
                <AccordionTrigger>{item.question}</AccordionTrigger>
                <AccordionContent>{item.answer}</AccordionContent>
              </AccordionItem>
            ))}
          </Accordion>
        </AccordionContent>
      </AccordionItem>
      <AccordionItem value="shipping">
        <AccordionTrigger>Shipping</AccordionTrigger>
        <AccordionContent>Ships within 2 business days.</AccordionContent>
      </AccordionItem>
    </Accordion>
  )
}
```

### Right to left

Content, icons and the open motion follow the reading direction.

```tsx title="components/examples/accordion/rtl.tsx"
import {
  Accordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
} from "@/components/ui/accordion"

const items = [
  {
    value: "what",
    question: "ما هي HextaUI؟",
    answer: "مجموعة مكونات مبنية على shadcn/ui مع اهتمام بالتفاصيل والحركة.",
  },
  {
    value: "install",
    question: "كيف أثبّت مكوّنًا؟",
    answer: "انسخ الشيفرة إلى مشروعك وعدّلها كأي ملف تملكه.",
  },
  {
    value: "license",
    question: "هل يمكنني استخدامها في مشاريع تجارية؟",
    answer: "نعم، جميع المكونات مجانية ومفتوحة المصدر.",
  },
]

export function AccordionRtl() {
  return (
    <div dir="rtl" className="w-full max-w-md">
      <Accordion variant="outline" defaultValue={["what"]}>
        {items.map((item) => (
          <AccordionItem key={item.value} value={item.value}>
            <AccordionTrigger>{item.question}</AccordionTrigger>
            <AccordionContent>{item.answer}</AccordionContent>
          </AccordionItem>
        ))}
      </Accordion>
    </div>
  )
}
```

## Keyboard

| Key | Action |
| --- | --- |
| `Enter` `Space` | Opens or closes the focused item. |
| `↓` | Moves focus to the next trigger. |
| `↑` | Moves focus to the previous trigger. |
| `Home` | Moves focus to the first trigger. |
| `End` | Moves focus to the last trigger. |
| `Tab` | Moves focus into the open panel, then on to the next trigger. |

## API reference

Built on the Base UI accordion. Every part accepts the props of the primitive it wraps.

### Accordion

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `variant` | `"default" \| "outline" \| "separated" \| "ghost"` | `"default"` |  |
| `multiple` | `boolean` | `false` | Allow more than one item to be open. |
| `value` | `Value[]` | – |  |
| `defaultValue` | `Value[]` | – |  |
| `onValueChange` | `(value: Value[], details) => void` | – |  |
| `disabled` | `boolean` | `false` |  |
| `hiddenUntilFound` | `boolean` | `true` | Keep closed panels findable with the browser’s page search. |
| `keepMounted` | `boolean` | `false` | Ignored while hiddenUntilFound is on. |
| `render` | `ReactElement \| (props, state) => ReactElement` | `<div>` |  |

| Attribute | Description |
| --- | --- |
| `data-slot="accordion"` | Target the root in CSS. |
| `data-variant` | The current variant. |
| `data-disabled` | Present when the accordion is disabled. |

### AccordionItem

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `Value` | – | Generated automatically when omitted. |
| `disabled` | `boolean` | `false` |  |
| `onOpenChange` | `(open: boolean, details) => void` | – |  |
| `render` | `ReactElement \| (props, state) => ReactElement` | `<div>` |  |

| Attribute | Description |
| --- | --- |
| `data-slot="accordion-item"` | Target items in CSS. |
| `data-open` | Present when the item is open. |
| `data-disabled` | Present when the item is disabled. |
| `data-index` | The item’s position, starting at 0. |

### AccordionTrigger

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `icon` | `ReactNode \| null` | `<IconChevronDown />` | null removes the icon. |
| `render` | `ReactElement \| (props, state) => ReactElement` | `<button>` |  |

| Attribute | Description |
| --- | --- |
| `data-slot="accordion-trigger"` | Target triggers in CSS. |
| `data-panel-open` | Present when its panel is open. Style the icon with group-data-panel-open/accordion-trigger. |
| `data-disabled` | Present when the item is disabled. |

### AccordionContent

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` | – | Applied to the inner wrapper, so padding never fights the height animation. |
| `keepMounted` | `boolean` | `false` |  |
| `hiddenUntilFound` | `boolean` | – |  |

| Attribute | Description |
| --- | --- |
| `data-slot="accordion-content"` | Target panels in CSS. |
| `data-open` | Present when the panel is open. |
| `data-starting-style` | Present while the panel animates in. |
| `data-ending-style` | Present while the panel animates out. |
| `data-settled` | Present once the panel is fully open. It stops clipping then, so focus rings inside a nested accordion show in full. |
| `--accordion-panel-height` | The panel’s measured height, used for the height animation. |

## Notes for AI assistants

- Install a component with the shadcn CLI: `npx shadcn@latest add https://hextaui.com/r/<name>.json`. It adds the source, the HextaUI theme tokens and any HextaUI components it depends on. `https://hextaui.com/r/all.json` installs every component.
- The code is then owned by the project, like shadcn/ui. There is no HextaUI npm package. HextaUI is MIT licensed and free for personal and commercial use.
- Behavior and accessibility come from Base UI (`@base-ui/react`). Compose with the `render` prop, not `asChild`.
- Styling uses Tailwind CSS v4 with theme tokens. Merge classes with `cn` from the `cn` package.
- Icons come from `@tabler/icons-react`.
- Import components from `@/components/ui/<name>`, hooks from `@/hooks/<name>` and utilities from `@/lib/<name>`.

Every HextaUI doc: https://hextaui.com/llms.txt
