Accordion
An accordion is a stack of sections where each heading opens its own content. It keeps a long page short and lets people read only what they need.
Yes. Every color comes from a theme token, so changing a token restyles every component.
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
export default function AccordionDemo() {
return (
<Accordion defaultValue={["colors"]} className="max-w-md">
<AccordionItem value="what">
<AccordionTrigger>What is nsdev/ui?</AccordionTrigger>
<AccordionContent>
A set of components built on Base UI and styled with Tailwind. The code is yours to read
and change.
</AccordionContent>
</AccordionItem>
<AccordionItem value="colors">
<AccordionTrigger>Can I change the colors?</AccordionTrigger>
<AccordionContent>
Yes. Every color comes from a theme token, so changing a token restyles every component.
</AccordionContent>
</AccordionItem>
<AccordionItem value="access">
<AccordionTrigger>Is it accessible?</AccordionTrigger>
<AccordionContent>
Base UI handles the keyboard and screen reader behavior, and the styles keep a visible
focus ring.
</AccordionContent>
</AccordionItem>
<AccordionItem value="soon" disabled>
<AccordionTrigger>Coming soon</AccordionTrigger>
<AccordionContent>This section is not available yet.</AccordionContent>
</AccordionItem>
</Accordion>
);
}The code
The component is a single file, read from the repository when the site is built. Press Show code to see all of it.
import type { ComponentProps } from "react";
import { Plus } from "lucide-react";
import { Accordion as BaseAccordion } from "@base-ui/react/accordion";
import { cx } from "@/lib/cx";
import { focusRing } from "@/lib/styles";
import type { WithClass } from "@/lib/types";
// A stack of sections that open and close. Only one is open at a time unless you pass `multiple`.
export function Accordion({ className, ...props }: WithClass<ComponentProps<typeof BaseAccordion.Root>>) {
return <BaseAccordion.Root className={cx("flex w-full flex-col", className)} {...props} />;
}
export function AccordionItem({ className, ...props }: WithClass<ComponentProps<typeof BaseAccordion.Item>>) {
return <BaseAccordion.Item className={cx("border-b border-border", className)} {...props} />;
}
// The heading wraps the button, as the pattern asks. The plus turns into a cross while open.
export function AccordionTrigger({
className,
children,
...props
}: WithClass<ComponentProps<typeof BaseAccordion.Trigger>>) {
return (
<BaseAccordion.Header className="flex">
<BaseAccordion.Trigger
className={cx(
"group flex flex-1 cursor-pointer items-center justify-between gap-4 rounded-sm py-4 text-left text-base font-medium text-text transition-colors",
focusRing,
"not-data-[disabled]:hover:text-text/70 data-[disabled]:cursor-not-allowed data-[disabled]:text-text-secondary",
className,
)}
{...props}
>
{children}
<Plus
aria-hidden
className="size-4 shrink-0 transition-transform duration-150 group-data-[panel-open]:rotate-45 motion-reduce:transition-none"
/>
</BaseAccordion.Trigger>
</BaseAccordion.Header>
);
}
// The panel animates its height, using the height Base UI measures for it.
export function AccordionContent({
className,
children,
...props
}: WithClass<ComponentProps<typeof BaseAccordion.Panel>>) {
return (
<BaseAccordion.Panel
className="h-[var(--accordion-panel-height)] overflow-hidden text-base text-text/70 transition-[height] duration-150 ease-out data-[ending-style]:h-0 data-[starting-style]:h-0 motion-reduce:transition-none"
{...props}
>
<div className={cx("pb-4", className)}>{children}</div>
</BaseAccordion.Panel>
);
}Props
| Prop | Type | Default | What it does |
|---|---|---|---|
multiple | boolean | false | Lets several sections be open at the same time. By default opening one closes the other. |
defaultValue | string[] | — | The values of the items that start open, when the accordion manages its own state. |
value | string[] | — | The values of the open items, when you control them yourself. |
onValueChange | function | — | Called with the new list of open values. |
value | string | — | On each AccordionItem. The name of that section. Required if you control the accordion. |
disabled | boolean | false | On the Accordion or on one AccordionItem. Stops it from opening or closing. |
Every other prop goes to the Base UI Accordion underneath.
The thinking
Some content is needed by a few people and ignored by the rest. An accordion hides it behind its heading without removing it.
- Built on Base UI: Base UI tracks which items are open, wires the heading to its panel, and measures the panel so it can animate to its real height.
- Four parts: Accordion holds the stack, AccordionItem is one section, AccordionTrigger is its heading and AccordionContent is what opens.
- One or several: By default one section is open at a time. Add multiple when people may want to compare two answers side by side.
- Not the same as Tabs: An accordion stacks sections vertically and reads top to bottom. Tabs show one panel at a time, in the same place.
Interface design
- Quiet: Each section is a heading and a hairline under it. There are no boxes, so a stack of them stays light.
- Plus to cross: The plus turns a quarter turn into a cross while the section is open, and sits after the heading text.
- Open: The panel text is the dimmer secondary text color, so the heading stays the strongest thing in the row.
- Disabled: The heading uses the muted text color, drops the hover change and shows the not-allowed cursor.
Interaction
- Open and close: Click a heading, or press Enter or Space when it has focus.
- Keyboard: Each heading is its own stop in the tab order, so Tab moves from one to the next.
- Hover: A heading dims slightly under the pointer to show it can be pressed.
- Motion: The panel grows and shrinks in 150ms. With reduced motion turned on it opens at once.
Empathy
- Headings: Each trigger sits inside a heading, so a screen reader user can jump between sections the way they jump between headings on a page.
- State is announced: Each trigger reports whether it is expanded, so the plus and cross are never the only signal.
- Focus: Keyboard focus draws a ring around the heading.
- Write real headings: The trigger text is the only thing visible while closed, so make it say what is inside.