Skip to content
Seamless UI

Accordion

Save

Stacked 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 accordion

Pre-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-run

Usage

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

PropTypeDefaultDescription
Accordion.multiplebooleanfalseLets more than one section be open at a time.
Accordion.defaultValueany[]—Values of the sections open initially, when uncontrolled.
Accordion.valueany[]—Values of the open sections, when controlled. Pair with onValueChange.
Accordion.onValueChange(value: any[], details) => void—Called when a section opens or closes.
Accordion.hiddenUntilFoundbooleanfalseLets the browser's find-in-page search reveal and open closed sections.
Accordion.disabledbooleanfalseDisables every section.
AccordionItem.valueany—Identifies the section in value and defaultValue. Generated when omitted.
AccordionItem.disabledbooleanfalseDisables this section.
AccordionTrigger.headingClassNamestring—Classes for the <h3> that wraps the trigger button.
AccordionPanel.panelClassNamestring—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.tsxShow
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>
  );
}