Accordion
SaveStacked sections on hairline dividers that expand and collapse with an animated height and content fade, one or several at a time.
- Category
- Core
- Version
- 0.2.0
- Frameworks
- Next.js, Vite + React
- Styling
- Tailwind CSS v4
Preview
Accordion preview
Run the CLI with the component name. The source lands in your project, ready to edit.
Installation
npx @seamlessui/cli add accordionPre-release@seamlessui/cli is not on npm yet. Install from a local build.
Review the plan first. A dry run lists every file and package without changing anything:
npx @seamlessui/cli add accordion --dry-runUsage
accordion-example.tsx
import { Accordion, AccordionItem, AccordionPanel, AccordionTrigger } from "@/components/seamless/ui/accordion";
export default function AccordionExample() {
return (
<Accordion defaultValue={["install"]} className="max-w-md">
<AccordionItem value="install">
<AccordionTrigger>How do I install a component?</AccordionTrigger>
<AccordionPanel>Run the CLI with the component name. The source lands in your project, ready to edit.</AccordionPanel>
</AccordionItem>
<AccordionItem value="update">
<AccordionTrigger>Can I update it later?</AccordionTrigger>
<AccordionPanel>Yes. The CLI shows a diff before it changes any file you have edited.</AccordionPanel>
</AccordionItem>
<AccordionItem value="motion">
<AccordionTrigger>Does it respect reduced motion?</AccordionTrigger>
<AccordionPanel>Panels open instantly when the system asks for reduced motion.</AccordionPanel>
</AccordionItem>
</Accordion>
);
}API
| Prop | Type | Default | Description |
|---|---|---|---|
| Accordion.multiple | boolean | false | Lets more than one section be open at a time. |
| Accordion.defaultValue | any[] | — | Values of the sections open initially, when uncontrolled. |
| Accordion.value | any[] | — | Values of the open sections, when controlled. Pair with onValueChange. |
| Accordion.onValueChange | (value: any[], details) => void | — | Called when a section opens or closes. |
| Accordion.hiddenUntilFound | boolean | false | Lets the browser's find-in-page search reveal and open closed sections. |
| Accordion.disabled | boolean | false | Disables every section. |
| AccordionItem.value | any | — | Identifies the section in value and defaultValue. Generated when omitted. |
| AccordionItem.disabled | boolean | false | Disables this section. |
| AccordionTrigger.headingClassName | string | — | Classes for the <h3> that wraps the trigger button. |
| AccordionPanel.panelClassName | string | — | Classes for the animated panel element. className styles the padded content inside. |
Accessibility
- Each trigger is a native <button> inside an <h3>, with aria-expanded and aria-controls pointing at its panel.
- Panels have role=region and are labelled by their trigger.
- Enter and Space toggle a section; Tab moves between triggers in document order.
- Closed panels are removed from the DOM (or hidden with hidden=until-found), so their content is not focusable or announced.
- The chevron is decorative; the open state is conveyed by aria-expanded and by the visible content.
- Panels open in 220ms and close in 180ms with a quint ease-out; with reduced motion they open and close instantly and the chevron flips without rotating.
Files
Installed into src/components/seamless, or components/seamless when the project has no src directory.
src/components/seamless/ui/accordion.tsxaccordion- npm packages: @base-ui/react@^1.9.0React and Tailwind CSS v4 required
Source
The exact source the CLI installs, from /r/items/react/accordion/0.2.0.json.
ui/accordion.tsxShowHide
ui/accordion.tsx
"use client";
import { Accordion as BaseAccordion } from "@base-ui/react/accordion";
function cx(...classes: Array<string | false | null | undefined>) {
return classes.filter(Boolean).join(" ");
}
export interface AccordionProps extends Omit<BaseAccordion.Root.Props, "className"> {
className?: string;
}
/**
* A stack of collapsible sections on hairline dividers. One item opens at a time unless `multiple` is set.
* `defaultValue` and `value` are arrays of item values.
*/
export function Accordion({ className, ...props }: AccordionProps) {
return (
<BaseAccordion.Root
className={cx(
"w-full divide-y divide-zinc-950/10 border-y border-zinc-950/10 dark:divide-white/10 dark:border-white/10",
className,
)}
{...props}
/>
);
}
export interface AccordionItemProps extends Omit<BaseAccordion.Item.Props, "className"> {
className?: string;
}
/** One section: an `AccordionTrigger` followed by an `AccordionPanel`. */
export function AccordionItem({ className, ...props }: AccordionItemProps) {
return <BaseAccordion.Item className={className} {...props} />;
}
export interface AccordionTriggerProps extends Omit<BaseAccordion.Trigger.Props, "className"> {
className?: string;
/** Classes for the heading element that wraps the trigger (an `<h3>` by default). */
headingClassName?: string;
}
/** The heading button that opens and closes its section. */
export function AccordionTrigger({ className, headingClassName, children, ...props }: AccordionTriggerProps) {
return (
<BaseAccordion.Header className={cx("m-0 flex", headingClassName)}>
<BaseAccordion.Trigger
className={cx(
"group flex flex-1 cursor-pointer items-center justify-between gap-4 rounded-lg py-3.5 text-left text-sm leading-5 font-medium",
"text-zinc-950 dark:text-white",
"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-500",
"data-disabled:cursor-not-allowed data-disabled:opacity-45",
className,
)}
{...props}
>
{children}
<span
aria-hidden="true"
className={cx(
"-my-1 inline-flex size-7 shrink-0 items-center justify-center rounded-md text-zinc-500",
"transition-[background-color,color] duration-150 ease-out motion-reduce:transition-none",
"group-hover:bg-zinc-950/[0.04] group-hover:text-zinc-950 group-data-panel-open:text-zinc-950",
"dark:group-hover:bg-white/[0.06] dark:group-hover:text-white dark:group-data-panel-open:text-white",
"group-data-disabled:bg-transparent",
)}
>
<svg
viewBox="0 0 16 16"
fill="none"
className={cx(
"size-4",
"transition-[rotate] duration-220 ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none",
"group-data-panel-open:rotate-180",
)}
>
<path d="m4.5 6.25 3.5 3.5 3.5-3.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</span>
</BaseAccordion.Trigger>
</BaseAccordion.Header>
);
}
export interface AccordionPanelProps extends Omit<BaseAccordion.Panel.Props, "className"> {
className?: string;
/** Classes for the panel element itself. `className` styles the padded content inside it. */
panelClassName?: string;
}
/**
* The section content. Its height animates open and closed (220ms, quint ease-out; closing is faster)
* while the content fades; instant with reduced motion.
*/
export function AccordionPanel({ className, panelClassName, children, ...props }: AccordionPanelProps) {
return (
<BaseAccordion.Panel
className={cx(
"h-(--accordion-panel-height) overflow-hidden",
"transition-[height,opacity] duration-220 ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none",
"data-starting-style:h-0 data-starting-style:opacity-0",
"data-ending-style:h-0 data-ending-style:opacity-0 data-ending-style:duration-180",
panelClassName,
)}
{...props}
>
<div className={cx("pr-11 pb-4 text-sm leading-relaxed text-zinc-600 dark:text-zinc-400", className)}>{children}</div>
</BaseAccordion.Panel>
);
}