Accordion

A group of disclosures, built with React Aria's DisclosureGroup.

We offer standard (5-7 days), express (2-3 days), and overnight shipping. Free shipping on international orders.

"use client"

import * as React from "react"
import { cn } from "cn"
import {
  DisclosurePanel as AccordionContentPrimitive,
  Heading as AccordionHeaderPrimitive,
  Disclosure as AccordionItemPrimitive,
  DisclosureGroup as AccordionPrimitive,
  Button as AccordionTriggerPrimitive,
  type ButtonProps,
  type DisclosureGroupProps,
  type DisclosurePanelProps,
  type DisclosureProps,
} from "@animate-ui/react/primitives/react-aria"

import { ArrowDown01Icon } from "@hugeicons/core-free-icons"
import { HugeiconsIcon } from "@hugeicons/react"

function Accordion({ className, ...props }: DisclosureGroupProps) {
  return (
    <AccordionPrimitive
      data-slot="accordion"
      className={cn("flex w-full flex-col", className)}
      {...props}
    />
  )
}

function AccordionItem({ className, ...props }: DisclosureProps) {
  return (
    <AccordionItemPrimitive
      data-slot="accordion-item"
      className={cn("not-last:border-b", className)}
      {...props}
    />
  )
}

function AccordionTrigger({
  className,
  children,
  ...props
}: Omit<ButtonProps, "children"> & { children: React.ReactNode }) {
  return (
    <AccordionHeaderPrimitive className="flex">
      <AccordionTriggerPrimitive
        slot="trigger"
        data-slot="accordion-trigger"
        className={cn(
          "focus-visible:ring-ring/50 focus-visible:border-ring focus-visible:after:border-ring **:data-[slot=accordion-trigger-icon]:text-muted-foreground rounded-lg py-2.5 text-left text-sm font-medium hover:underline focus-visible:ring-3 **:data-[slot=accordion-trigger-icon]:ml-auto **:data-[slot=accordion-trigger-icon]:size-4 group/accordion-trigger relative flex flex-1 items-start justify-between border border-transparent transition-all outline-none disabled:pointer-events-none disabled:opacity-50",
          className
        )}
        {...props}
      >
        {children}
        <HugeiconsIcon icon={ArrowDown01Icon} strokeWidth={2}
          data-slot="accordion-trigger-icon"
          className="pointer-events-none shrink-0 transition-transform duration-350 ease-[cubic-bezier(0.215,0.61,0.355,1)] group-aria-expanded/accordion-trigger:rotate-180"
        />
      </AccordionTriggerPrimitive>
    </AccordionHeaderPrimitive>
  )
}

function AccordionContent({
  className,
  children,
  ...props
}: DisclosurePanelProps) {
  return (
    <AccordionContentPrimitive
      data-slot="accordion-content"
      className="text-sm overflow-clip"
      {...props}
    >
      <div
        className={cn(
          "pt-0 pb-2.5 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4",
          className
        )}
      >
        {children}
      </div>
    </AccordionContentPrimitive>
  )
}

export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }

export default function AccordionDemo({
  multiple = false,
}: {
  multiple?: boolean
}) {
  return (
    <Accordion
      key={String(multiple)}
      allowsMultipleExpanded={multiple}
      defaultExpandedKeys={["shipping"]}
      className="max-w-lg"
    >
      <AccordionItem id="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 id="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 id="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

Install the packages

npm install @animate-ui/react motion
pnpm add @animate-ui/react motion
yarn add @animate-ui/react motion
bun add @animate-ui/react motion

Add the shadcn/ui component

npx shadcn@latest add accordion
pnpm dlx shadcn@latest add accordion
yarn dlx shadcn@latest add accordion
bun x shadcn@latest add accordion

Animate it

The CLI finds your shadcn/ui components through components.json, swaps their primitive imports for Animate UI's and removes the CSS animation classes Motion replaces.

npx @animate-ui/cli@latest animate accordion
pnpm dlx @animate-ui/cli@latest animate accordion
yarn dlx @animate-ui/cli@latest animate accordion
bun x @animate-ui/cli@latest animate accordion

Usage

Import from Animate UI instead of react-aria-components: it re-exports all of React Aria, with an animated DisclosurePanel. Everything else stays the same.

import {
  Button,
  Disclosure,
  DisclosureGroup,
  DisclosurePanel,
  Heading,
} from '@animate-ui/react/primitives/react-aria';

export function FAQ() {
  return (
    <DisclosureGroup defaultExpandedKeys={['accessible']}>
      <Disclosure id="accessible">
        <Heading>
          <Button slot="trigger">Is it accessible?</Button>
        </Heading>
        <DisclosurePanel>
          Yes. It adheres to the WAI-ARIA design pattern.
        </DisclosurePanel>
      </Disclosure>
    </DisclosureGroup>
  );
}

Animation

  • DisclosurePanel unfolds: its height grows from 0 while the content fades in from a 2px blur, over 350ms on a soft ease-out. While it moves, its bottom edge fades (over 16px, or over the part still hidden): the content never meets what follows it, an item's border, with a hard cut. It folds back the same way, in reverse.
  • Opening another disclosure while one is closing retargets smoothly.
  • A disclosure expanded by default animates open on mount (page load), as in v1.
  • React Aria keeps closed panels mounted with hidden="until-found": the panel stays visible (and inert) while it collapses, then gets hidden="until-found" back, so find-in-page can still reveal it. A panel revealed by find-in-page opens at once, as in React Aria.
  • Motion animates the panel itself: shadcn/ui's h-(--disclosure-panel-height) transition-[height] classes are no longer needed (the CLI removes them). --disclosure-panel-height and --disclosure-panel-width read auto.
  • <AnimateUIProvider tempo> paces both transitions.
  • Under reduced motion, the height changes at once; the fade and the blur still play.

API Reference

Every component takes exactly the props of React Aria's DisclosureGroup. Animate UI adds the animation props below.

DisclosureGroup, Disclosure, Heading, Button

React Aria props only. They are not animated.

DisclosurePanel

React Aria props, plus:

PropTypeDefault
transition?
Transition{ duration: 0.35, ease: [0.215, 0.61, 0.355, 1] }
exitTransition?
Transitiontransition

Built by Skyleen. The source code is available on GitHub.

Last updated: 10/18/2026

On this page