Accordion
LayoutSingle or multiple expandable sections with keyboard navigation.
Responsive viewport — resize the window to explore. Direction and density apply only to this preview. Theme applies to the docs too.
"use client";
import {
Accordion,
AccordionItem,
AccordionTrigger,
AccordionContent,
} from "@/components/ui/accordion";
export default function AccordionExample() {
return (
<div className="w-full max-w-md space-y-6">
<div>
<p className="mb-2 text-xs font-medium text-muted-foreground">
Single open section
</p>
<Accordion type="single" collapsible defaultValue="shipping">
{[
{
id: "shipping",
title: "How does shipping work?",
text: "Track every order from your workspace. Standard delivery takes 3–5 business days.",
},
{
id: "returns",
title: "Can I return an order?",
text: "Eligible items can be returned within 30 days. Contact your support team to get started.",
},
].map((item) => (
<AccordionItem key={item.id} value={item.id}>
<AccordionTrigger>{item.title}</AccordionTrigger>
<AccordionContent>{item.text}</AccordionContent>
</AccordionItem>
))}
</Accordion>
</div>
<Accordion type="multiple" defaultValue={["security", "billing"]}>
<AccordionItem value="security">
<AccordionTrigger>Security</AccordionTrigger>
<AccordionContent>
Two-factor authentication protects your workspace.
</AccordionContent>
</AccordionItem>
<AccordionItem value="billing">
<AccordionTrigger>Billing</AccordionTrigger>
<AccordionContent>
Manage invoices and payment methods.
</AccordionContent>
</AccordionItem>
<AccordionItem value="locked" disabled>
<AccordionTrigger>Restricted settings</AccordionTrigger>
<AccordionContent>
Only administrators can edit these settings.
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
);
}Usage
Compose AccordionItem, AccordionTrigger, and AccordionContent. Use type=single with collapsible for an optional open section, or type=multiple for independent sections. Each item needs a unique value.
import {
Accordion,
AccordionItem,
AccordionTrigger,
AccordionContent,
} from "@/components/ui/accordion";Shipped as editable source in src/components/ui/accordion.tsx in both apps. Copy the implementation below when moving it to another project. Copy the complete example from Source code for all imports and state. Add "use client" when using hooks or event handlers. See installation and theming for project setup.
Component source
The complete implementation is included in both apps. It uses the React, Radix, Lucide, and shared-core dependencies already installed in Apex. Keep your project’s semantic theme tokens when reusing it.
src/components/ui/accordion.tsx
"use client";
import { useDocumentDirection } from "@/lib/use-document-direction";
import type { ComponentProps } from "react";
import { cn } from "@dashboardpack/core/lib/utils";
import { Accordion as Primitive } from "radix-ui";
import { ChevronDown } from "lucide-react";
export function Accordion(props: ComponentProps<typeof Primitive.Root>) {
const dir = useDocumentDirection();
return <Primitive.Root dir={dir} {...props} />;
}
export function AccordionItem({
className,
...props
}: ComponentProps<typeof Primitive.Item>) {
return (
<Primitive.Item
className={cn("border-b last:border-b-0", className)}
{...props}
/>
);
}
export function AccordionTrigger({
className,
children,
...props
}: ComponentProps<typeof Primitive.Trigger>) {
return (
<Primitive.Header className="flex">
<Primitive.Trigger
className={cn(
"group flex flex-1 items-center justify-between gap-4 rounded-sm py-4 text-start text-sm font-medium outline-none hover:text-primary focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
className,
)}
{...props}
>
{children}
<ChevronDown
aria-hidden
className="size-4 shrink-0 transition-transform group-data-[state=open]:rotate-180 motion-reduce:transition-none"
/>
</Primitive.Trigger>
</Primitive.Header>
);
}
export function AccordionContent({
className,
children,
...props
}: ComponentProps<typeof Primitive.Content>) {
return (
<Primitive.Content
className={cn(
"overflow-hidden pb-4 text-sm leading-relaxed text-muted-foreground",
className,
)}
{...props}
>
{children}
</Primitive.Content>
);
}Required helper: src/lib/use-document-direction.ts
Copy this helper alongside the component. It keeps keyboard navigation aligned with the page direction and supports live RTL changes.
"use client";
import { useSyncExternalStore } from "react";
function subscribe(notify: () => void) {
const observer = new MutationObserver(notify);
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["dir"],
});
return () => observer.disconnect();
}
function snapshot(): "ltr" | "rtl" {
return document.documentElement.dir === "rtl" ? "rtl" : "ltr";
}
/** Keep headless controls aligned with the document, including live RTL changes. */
export function useDocumentDirection() {
return useSyncExternalStore(subscribe, snapshot, () => "ltr" as const);
}API reference
Common props and defaults. Slot-specific props are named below; native elements also accept their standard HTML attributes. The component TypeScript definitions are the complete type reference.
| Prop | Type / Default | Description |
|---|---|---|
| type | single | multipleDefault: required | Selection mode; determines the value shape. |
| value / defaultValue | string | string[]Default: — | Controlled or initial open items. |
| onValueChange | (value: string | string[]) => voidDefault: — | Mode-specific change callback. |
| collapsible | boolean (single mode)Default: false | Allow closing the open item. |