Installation
pnpm dlx shadcn@latest add https://rdf.dev.360online.com/r/accordion.jsonInstalls to @/components/ui/accordion.
Manual installation
Install the dependencies:
pnpm add @base-ui/react@^1.8.0 lucide-react@^1.48.0Copy the source into your project:
'use client'
import { Accordion as AccordionPrimitive } from '@base-ui/react/accordion'
import { ChevronDownIcon, ChevronUpIcon } from 'lucide-react'
import { cn } from '@/lib/utils'
function Accordion({ className, ...props }: AccordionPrimitive.Root.Props) {
return <AccordionPrimitive.Root data-slot="accordion" className={cn('flex w-full flex-col', className)} {...props} />
}
function AccordionItem({ className, ...props }: AccordionPrimitive.Item.Props) {
return <AccordionPrimitive.Item data-slot="accordion-item" className={cn('not-last:border-b', className)} {...props} />
}
interface AccordionTriggerProps extends AccordionPrimitive.Trigger.Props {
/** The heading level wrapping the trigger, to fit the page's outline. Defaults to 3. */
headingLevel?: 2 | 3 | 4
}
function AccordionTrigger({ className, children, headingLevel = 3, ...props }: Readonly<AccordionTriggerProps>) {
const Heading = `h${headingLevel}` as const
return (
<AccordionPrimitive.Header className="flex" render={(headerProps) => <Heading {...headerProps} />}>
<AccordionPrimitive.Trigger
data-slot="accordion-trigger"
className={cn(
'group/accordion-trigger relative flex flex-1 items-start justify-between rounded-lg border border-transparent py-2.5 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring focus-visible:after:border-ring aria-disabled:pointer-events-none aria-disabled:opacity-50 **:data-[slot=accordion-trigger-icon]:ml-auto **:data-[slot=accordion-trigger-icon]:size-4 **:data-[slot=accordion-trigger-icon]:text-muted-foreground',
className,
)}
{...props}
>
{children}
<ChevronDownIcon
aria-hidden="true"
data-slot="accordion-trigger-icon"
className="pointer-events-none shrink-0 group-aria-expanded/accordion-trigger:hidden"
/>
<ChevronUpIcon
aria-hidden="true"
data-slot="accordion-trigger-icon"
className="pointer-events-none hidden shrink-0 group-aria-expanded/accordion-trigger:inline"
/>
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
)
}
function AccordionContent({ className, children, ...props }: AccordionPrimitive.Panel.Props) {
return (
<AccordionPrimitive.Panel
data-slot="accordion-content"
className="overflow-hidden text-sm data-open:animate-accordion-down data-closed:animate-accordion-up"
{...props}
>
{/* No fixed height here: the open/close keyframes on the panel use --accordion-panel-height, and at rest the
content keeps its natural height so it can reflow (zoom, text spacing, content that grows) */}
<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>
</AccordionPrimitive.Panel>
)
}
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }A vertically stacked set of interactive headings that each reveal a section of content. Built on top of Base UI Accordion, it follows WAI-ARIA design patterns and animates open and closed.
Features
- One expanded section at a time by default, or several with
multiple - Every open section can be closed again
- Height animation on open/close (
animate-accordion-down/animate-accordion-up) - Chevron indicator that switches from down to up when the item is expanded
- Items are separated by a bottom border (except the last one)
- Content styles for rich text: links are underlined and consecutive paragraphs are spaced
- Disabled items
- Keyboard navigation and screen reader support
data-slotattributes (accordion,accordion-item,accordion-trigger,accordion-content) for styling hooks
Usage
import {
Accordion,
AccordionItem,
AccordionTrigger,
AccordionContent,
} from '@/components/ui/accordion'
// One section open at a time
export function PermitFaq() {
return (
<Accordion>
<AccordionItem value="need-permit">
<AccordionTrigger>Do I need a building permit for a garden shed?</AccordionTrigger>
<AccordionContent>Not if it is smaller than 15 m² and lower than 3 metres.</AccordionContent>
</AccordionItem>
<AccordionItem value="processing-time">
<AccordionTrigger>How long does it take to get a decision?</AccordionTrigger>
<AccordionContent>Most complete applications are decided within 4 weeks.</AccordionContent>
</AccordionItem>
</Accordion>
)
}
// Several sections open at once
export function CaseDetails() {
return (
<Accordion multiple defaultValue={['applicant']}>
<AccordionItem value="applicant">
<AccordionTrigger>Applicant</AccordionTrigger>
<AccordionContent>Anna Lindqvist</AccordionContent>
</AccordionItem>
<AccordionItem value="property">
<AccordionTrigger>Property</AccordionTrigger>
<AccordionContent>Eken 4:12, Storgatan 14</AccordionContent>
</AccordionItem>
</Accordion>
)
}Examples
Default
A FAQ for a building permit e-service. By default one answer is open at a time, and defaultValue={['need-permit']} opens the first one. value and defaultValue are always arrays.
Multiple Sections
With multiple several sections can be open at once, which suits a case summary where the handling officer
compares applicant and property details side by side.
Bordered
Wrap the accordion in a border and pad the items with className for a card-like list. Triggers can hold an icon next
to the label.
Disabled State
Disable an AccordionItem for a step that is not available yet, here the offer in a preschool placement application.
Rich Content
Paragraphs and links inside AccordionContent get spacing and underline styles automatically, and you can add actions
such as a button.
Props
Accordion
multiple- Whether several items can be open at once (defaultfalse)defaultValue- The values of the items to open by default (an array, also when only one item can be open)value- The controlled values of the open items (an array)onValueChange- Event handler for when the opened items change,(value, eventDetails) => voidhiddenUntilFound- Keep closed panels findable with the browser's find-in-page, which opens the matching itemdisabled- Disables every itemclassName- Additional CSS classes (the root is a full-width flex column)
AccordionItem
value- Unique identifier for the itemdisabled- Whether the item is disabledclassName- Additional CSS classes
AccordionTrigger
children- Content to display in the trigger. A chevron icon is appended automatically.headingLevel- The heading level wrapping the trigger:2,3or4(default3). Pick the level that follows the heading above the accordion, so the page outline skips no level, for example2when it sits directly under the page's H1.className- Additional CSS classes
AccordionContent
children- Content to display when expandedclassName- Additional CSS classes
Accessibility
The Accordion component follows WAI-ARIA guidelines:
- Uses proper ARIA roles (
region,button) - Supports keyboard navigation:
Space/Enter: Toggle sectionTab: Navigate between sectionsHome/End: Move to first/last section
- Announces state changes to screen readers
- Maintains focus management
- Provides visual feedback for interaction states
Best Practices
-
Content Organization:
- Group related content together
- Use clear, descriptive headers
- Keep content concise and focused
-
Visual Hierarchy:
- Use consistent spacing
- Maintain clear section boundaries
- Provide visual feedback for state changes
-
Accessibility:
- Write descriptive trigger text
- Ensure keyboard navigation
- Maintain sufficient touch targets
-
Performance:
- Lazy load content when possible
- Use appropriate animations
- Consider content height changes
-
User Experience:
- Allow section collapse when appropriate
- Provide smooth transitions
- Consider default open states
Related Components
- Tabs
- Collapsible