Use an accordion when readers need to scan several related sections and expand only the details they need. Use Collapsible for one independent disclosure. Each item needs a stable, unique value.
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
const questions = [
{
value: "source",
question: "Do I own the installed source?",
answer:
"Yes. The CLI copies the component files into your application so you can edit them directly.",
},
{
value: "theme",
question: "Can I change the visual style?",
answer:
"Use a preset or adjust the shared color, radius, type, and shadow tokens for your product.",
},
{
value: "framework",
question: "Does it work outside Next.js?",
answer: "Yes. The registry is verified in fresh Next.js and Vite consumers.",
},
];
export default function AccordionDemo() {
return (
<Accordion className="w-full max-w-xl">
{questions.map((item) => (
<AccordionItem key={item.value} value={item.value}>
<AccordionTrigger>{item.question}</AccordionTrigger>
<AccordionContent>{item.answer}</AccordionContent>
</AccordionItem>
))}
</Accordion>
);
}
#Installation
Complete the base setup first. The CLI installs this item and its dependencies. For manual installation:
First time? Install the shared base once.
npx shadcn@latest add https://neobrutal-ui.andongmin.com/r/accordion.jsonnpm install @base-ui/react@^1.8.0Create components/ui/accordion.tsx:
"use client";
import { Accordion as AccordionPrimitive } from "@base-ui/react/accordion";
import { ChevronDownIcon } from "lucide-react";
import { cn } from "@/lib/utils";
function Accordion({ className, ...props }: AccordionPrimitive.Root.Props<string>) {
return (
<AccordionPrimitive.Root<string>
data-slot="accordion"
{...props}
className={(state) =>
cn("flex w-full flex-col", typeof className === "function" ? className(state) : className)
}
/>
);
}
function AccordionItem({ className, ...props }: AccordionPrimitive.Item.Props) {
return (
<AccordionPrimitive.Item
data-slot="accordion-item"
{...props}
className={(state) =>
cn(
"overflow-hidden rounded-base border-2 border-b border-border shadow-shadow",
typeof className === "function" ? className(state) : className,
)
}
/>
);
}
function AccordionTrigger({ className, children, ...props }: AccordionPrimitive.Trigger.Props) {
return (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
data-slot="accordion-trigger"
{...props}
className={(state) =>
cn(
"group/accordion-trigger flex flex-1 items-center justify-between border-border bg-main p-4 text-left text-base font-heading text-main-foreground transition-all focus-visible:ring-[3px] aria-disabled:pointer-events-none aria-disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 data-panel-open:rounded-b-none data-panel-open:border-b-2 **:data-[slot=accordion-trigger-icon]:ml-auto **:data-[slot=accordion-trigger-icon]:size-5",
typeof className === "function" ? className(state) : className,
)
}
>
{children}
<ChevronDownIcon
data-slot="accordion-trigger-icon"
className="pointer-events-none shrink-0 transition-transform duration-200 group-data-panel-open/accordion-trigger:rotate-180"
aria-hidden="true"
/>
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
);
}
function AccordionContent({ className, children, ...props }: AccordionPrimitive.Panel.Props) {
return (
<AccordionPrimitive.Panel
data-slot="accordion-content"
{...props}
className={(state) =>
cn(
"h-(--accordion-panel-height) overflow-hidden rounded-b-base bg-secondary-background text-sm font-base transition-[height] duration-200 ease-out data-ending-style:h-0 data-starting-style:h-0",
typeof className === "function" ? className(state) : className,
)
}
>
<div className="p-4 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4">
{children}
</div>
</AccordionPrimitive.Panel>
);
}
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
#Usage
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
export default function HelpSections() {
return (
<Accordion className="w-full">
<AccordionItem value="source">
<AccordionTrigger>Can I edit the source?</AccordionTrigger>
<AccordionContent>Yes. Installed files belong to your application.</AccordionContent>
</AccordionItem>
<AccordionItem value="theme">
<AccordionTrigger>Does it support dark mode?</AccordionTrigger>
<AccordionContent>Use the matching light and dark theme tokens.</AccordionContent>
</AccordionItem>
</Accordion>
);
}#API reference
| Prop / part | Purpose | Default |
|---|---|---|
value / defaultValue | Controlled or initial open item values | Uncontrolled |
onValueChange | Receives the next open item values and event details | — |
multiple | Allow more than one item to stay open | false |
AccordionItem.disabled | Disables the section | false |
AccordionContent.keepMounted | Retains closed panel DOM | false |
className accepts a string or a function of the part's Base UI state. On AccordionContent, both forms style the panel element; target its direct child to customize the inner padding.
#Accessibility
Write the section name in AccordionTrigger; it is a button inside a heading. Tab and Shift+Tab move through the triggers and controls in open panels. Enter and Space toggle the focused item. Base UI 1.8 does not use arrow-key, Home, or End navigation between accordion triggers; its deprecated loopFocus and orientation props do not change keyboard focus behavior. Do not nest another button or link inside the trigger. In controlled mode, treat the value as the array of currently open item values.