A set of collapsible panels with headings.
Installation
npx shadcn@latest add @caprice/accordionpnpm dlx shadcn@latest add @caprice/accordionyarn dlx shadcn@latest add @caprice/accordionbunx shadcn@latest add @caprice/accordionnpm install @caprice-ui/reactpnpm add @caprice-ui/reactyarn add @caprice-ui/reactbun add @caprice-ui/reactInstall the following dependencies:
npm install @base-ui/react lucide-reactpnpm add @base-ui/react lucide-reactyarn add @base-ui/react lucide-reactbun add @base-ui/react lucide-reactAdd a cn helper
import { defineConfig, getSchema, type VariantProps } from 'cva';
import { twMerge } from 'tailwind-merge';
export const { cva, cx: cn } = defineConfig({
hooks: {
onComplete: (className: string) => twMerge(className),
},
});
export type { VariantProps };
export { getSchema };
Copy and paste the following code into your project.
'use client';
import { Accordion as AccordionPrimitive } from '@base-ui/react/accordion';
import { ChevronDownIcon } from 'lucide-react';
import type * as React from 'react';
import { cn } from '@/lib/utils';
/**
* Groups all parts of the accordion.
* Renders a `<div>` element.
*
* Documentation: [Caprice UI Accordion](https://caprice-ui.com/docs/components/accordion)
*
* API Reference: [Base UI Accordion](https://base-ui.com/react/components/accordion#root)
*/
export namespace Root {
export type State = AccordionPrimitive.Root.State;
export type Props = React.ComponentProps<typeof AccordionPrimitive.Root>;
export type ChangeEventDetails = AccordionPrimitive.Root.ChangeEventDetails;
export type ChangeEventReason = AccordionPrimitive.Root.ChangeEventReason;
}
export function Root({ ...props }: Root.Props) {
return <AccordionPrimitive.Root data-slot="accordion" {...props} />;
}
/**
* Groups an accordion header with the corresponding panel.
* Renders a `<div>` element.
*
* Documentation: [Caprice UI Accordion](https://caprice-ui.com/docs/components/accordion)
*
* API Reference: [Base UI Accordion](https://base-ui.com/react/components/accordion#item)
*/
export namespace Item {
export type State = AccordionPrimitive.Item.State;
export type Props = React.ComponentProps<typeof AccordionPrimitive.Item>;
export type ChangeEventDetails = AccordionPrimitive.Item.ChangeEventDetails;
export type ChangeEventReason = AccordionPrimitive.Item.ChangeEventReason;
}
export function Item({ className, ...props }: Item.Props) {
return (
<AccordionPrimitive.Item
className={cn('border-b last:border-b-0', className)}
data-slot="accordion-item"
{...props}
/>
);
}
/**
* A button that opens and closes the corresponding panel.
* Renders a `<button>` element.
*
* Documentation: [Caprice UI Accordion](https://caprice-ui.com/docs/components/accordion)
*
* API Reference: [Base UI Accordion](https://base-ui.com/react/components/accordion#trigger)
*/
export namespace Trigger {
export type Props = React.ComponentProps<typeof AccordionPrimitive.Trigger>;
}
export function Trigger({ className, children, ...props }: Trigger.Props) {
return (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
className={cn(
'group/accordion flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-start font-medium text-sm outline-none transition-all focus-visible:ring-[3px] focus-visible:ring-ring disabled:opacity-60',
className
)}
data-slot="accordion-trigger"
{...props}
>
{children}
<ChevronDownIcon
className="pointer-events-none size-4 shrink-0 translate-y-0.5 opacity-60 transition-transform duration-200 ease-in-out group-data-panel-open/accordion:rotate-180 motion-reduce:transition-none"
data-slot="accordion-trigger-icon"
/>
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
);
}
/**
* A collapsible panel with the accordion item contents.
* Renders a `<div>` element.
*
* Documentation: [Caprice UI Accordion](https://caprice-ui.com/docs/components/accordion)
*
* API Reference: [Base UI Accordion](https://base-ui.com/react/components/accordion#panel)
*/
export namespace Panel {
export type State = AccordionPrimitive.Panel.State;
export type Props = React.ComponentProps<typeof AccordionPrimitive.Panel>;
}
export function Panel({ className, children, ...props }: Panel.Props) {
return (
<AccordionPrimitive.Panel
className="h-(--accordion-panel-height) overflow-hidden text-muted-foreground text-sm transition-[height] duration-200 ease-in-out motion-reduce:transition-none [[data-starting-style],[data-ending-style]]:h-0"
data-slot="accordion-panel"
{...props}
>
<div className={cn('pt-0 pb-4', className)}>{children}</div>
</AccordionPrimitive.Panel>
);
}
/**
* @deprecated Use {@link Panel} instead
* @see {@link https://caprice-ui.com/docs/components/accordion}
*/
export const Content = Panel;
Update the import paths to match your project setup.
Usage
import * as Accordion from "@/components/caprice-ui/accordion"import * as Accordion from "@caprice-ui/react/accordion"<Accordion.Root>
<Accordion.Item value="item-1">
<Accordion.Trigger>What is Base UI?</Accordion.Trigger>
<Accordion.Panel>
Base UI is a library of high-quality unstyled React components for design systems
and web apps.
</Accordion.Panel>
</Accordion.Item>
</Accordion.Root>Examples
Single
Multiple
You can set up the accordion to allow multiple panels to be open at the same time using the multiple prop.
Controlled
Differences with shadcn/ui / Radix
If you're familiar with Radix UI and shadcn/ui, most patterns will carry over. This guide points out the differences so you can start using Caprice UI without surprises.
Key changes
| Feature | shadcn/ui | Caprice UI |
|---|---|---|
| Primitive | Radix Accordion | Base UI Accordion |
| Multiple items | type="multiple" | multiple={true} |
| Single item | type="single" | Default (no prop needed) |
| Collapsible | collapsible prop | Always collapsible |
| Default value | String (single) or Array (multiple) | Always Array |
| Composition | asChild prop | render prop |
| Panel component | AccordionContent | AccordionPanel (AccordionContent deprecated) |
| Open state attribute | data-state="open" | data-panel-open |
| Animation | CSS keyframe animations | CSS height transition via --accordion-panel-height |
| Hidden until found | Not supported | hiddenUntilFound={true} |
| Reduced motion | Not included | motion-reduce:transition-none |
| Disabled opacity | opacity-50 | opacity-60 |
The hiddenUntilFound prop enables browser find-in-page (⌘/Ctrl + F) to search content inside collapsed panels. When a match is found, the browser automatically expands the accordion item.
Comparison Example
<Accordion type="multiple" collapsible defaultValue="item-1">
<AccordionItem value="item-1">
<AccordionTrigger>Is it accessible?</AccordionTrigger>
<AccordionContent>Yes. It adheres to the WAI-ARIA pattern.</AccordionContent>
</AccordionItem>
</Accordion><Accordion.Root multiple defaultValue={['item-1']}>
<Accordion.Item value="item-1">
<Accordion.Trigger>Is it accessible?</Accordion.Trigger>
<Accordion.Panel>Yes. It adheres to the WAI-ARIA pattern.</Accordion.Panel>
</Accordion.Item>
</Accordion.Root>Accessibility
- Keyboard navigation: Use ↑/↓ Arrow keys to move focus between accordion triggers, Home/End to jump to the first/last trigger, and Enter or Space to expand or collapse the focused panel.
- ARIA attributes: The component automatically manages
aria-expanded,aria-controls, andaria-labelledbyto communicate state to assistive technologies. Each trigger is wrapped in a heading element for proper document structure. - Focus management: Focus remains on the trigger when expanding or collapsing panels, ensuring users don't lose their place in the document.
- Hidden until found: Use the
hiddenUntilFoundprop on panels to enable browser find-in-page (⌘/Ctrl + F) to search content inside collapsed panels. When a match is found, the browser automatically expands the accordion item. - Reduced motion: The component respects
prefers-reduced-motionviamotion-reduce:transition-none, disabling animations for users who prefer reduced motion. - Disabled items: Use the
disabledprop on individual items to prevent interaction. Disabled triggers remain focusable so screen reader users can discover them and understand why they're unavailable.
Last updated on