Use Collapsible for one independent section of optional content. Use Accordion for a related set of expandable sections. Visibility can be controlled by application state or left uncontrolled.
Live preview
"use client";
import { Check, ChevronsUpDown, Circle } from "lucide-react";
import * as React from "react";
import { Button } from "@/components/ui/button";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
const hiddenTasks = ["Production registry responds", "Release notes reviewed"];
export default function CollapsibleDemo() {
const [isOpen, setIsOpen] = React.useState(false);
return (
<Collapsible open={isOpen} onOpenChange={setIsOpen} className="w-full max-w-md space-y-2">
<div className="flex items-center justify-between gap-4 rounded-base border-2 border-border bg-main px-4 py-3 text-main-foreground shadow-shadow">
<div className="min-w-0">
<h3 className="font-heading">Release checklist</h3>
<p className="mt-1 text-sm">1 complete · 2 {isOpen ? "shown" : "hidden"}</p>
</div>
<CollapsibleTrigger
render={
<Button
variant="neutral"
size="icon-sm"
aria-label={isOpen ? "Hide remaining release tasks" : "Show remaining release tasks"}
/>
}
>
<ChevronsUpDown aria-hidden="true" />
</CollapsibleTrigger>
</div>
<div className="flex items-center gap-3 rounded-base border-2 border-border bg-secondary-background px-4 py-3 text-sm">
<Check className="size-4 shrink-0" aria-hidden="true" />
<span>Fresh consumer builds pass</span>
</div>
<CollapsibleContent className="space-y-2">
{hiddenTasks.map((task) => (
<div
key={task}
className="flex items-center gap-3 rounded-base border-2 border-border bg-secondary-background px-4 py-3 text-sm"
>
<Circle className="size-4 shrink-0" aria-hidden="true" />
<span>{task}</span>
</div>
))}
</CollapsibleContent>
</Collapsible>
);
}
#Installation
Complete the base setup. For manual installation:
First time? Install the shared base once.
npx shadcn@latest add https://neobrutal-ui.andongmin.com/r/collapsible.jsonnpm install @base-ui/react@^1.8.0Create components/ui/collapsible.tsx:
"use client";
import { Collapsible as CollapsiblePrimitive } from "@base-ui/react/collapsible";
import { cn } from "@/lib/utils";
function Collapsible({ className, ...props }: CollapsiblePrimitive.Root.Props) {
return (
<CollapsiblePrimitive.Root
data-slot="collapsible"
{...props}
className={(state) => cn(typeof className === "function" ? className(state) : className)}
/>
);
}
function CollapsibleTrigger(props: CollapsiblePrimitive.Trigger.Props) {
return <CollapsiblePrimitive.Trigger data-slot="collapsible-trigger" {...props} />;
}
function CollapsibleContent({ className, ...props }: CollapsiblePrimitive.Panel.Props) {
return (
<CollapsiblePrimitive.Panel
data-slot="collapsible-content"
{...props}
className={(state) =>
cn(
"h-(--collapsible-panel-height) overflow-hidden transition-[height] duration-200 ease-out data-ending-style:h-0 data-starting-style:h-0",
typeof className === "function" ? className(state) : className,
)
}
/>
);
}
export { Collapsible, CollapsibleTrigger, CollapsibleContent };
The Usage example additionally uses Button.
#Usage
import { Button } from "@/components/ui/button";
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from "@/components/ui/collapsible";
export default function AdvancedOptions() {
return (
<Collapsible className="grid max-w-sm gap-3">
<CollapsibleTrigger render={<Button variant="outline" />}>
Advanced options
</CollapsibleTrigger>
<CollapsibleContent>
<p>Additional workspace preferences belong here.</p>
</CollapsibleContent>
</Collapsible>
);
}#API reference
| Prop / part | Purpose | Default |
|---|---|---|
open / onOpenChange | Controlled expanded state | Uncontrolled |
defaultOpen | Initial expanded state | false |
disabled | Prevent toggling | false |
CollapsibleTrigger.render | Button composition | Primitive button |
CollapsibleContent.keepMounted | Preserve closed content DOM | false |
#Accessibility
The trigger must describe the content it reveals. Keep it keyboard accessible and do not nest other controls inside it. Enter and Space toggle the panel. Do not hide required instructions or validation errors in a closed section without giving the user a clear way to discover them.