Collapsible

A collapsible is one section with a button that opens and closes it. Use it when a single piece of extra content should stay out of the way until it is wanted.

import { Collapsible, CollapsiblePanel, CollapsibleTrigger } from "@/components/ui/collapsible";

export default function CollapsibleDemo() {
  return (
    <Collapsible className="max-w-xs">
      <CollapsibleTrigger>Recovery keys</CollapsibleTrigger>
      <CollapsiblePanel>
        <ul className="flex flex-col gap-1 py-2 font-mono text-sm text-text/70">
          <li>alien-bean-pasta</li>
          <li>wild-irish-burrito</li>
          <li>horse-battery-staple</li>
        </ul>
      </CollapsiblePanel>
    </Collapsible>
  );
}

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 { ChevronRight } from "lucide-react";
import { Collapsible as BaseCollapsible } from "@base-ui/react/collapsible";
import { Button } from "@/components/ui/button";
import { cx } from "@/lib/cx";
import type { WithClass } from "@/lib/types";

// One section that opens and closes. For several in a stack, use an Accordion instead.
export function Collapsible({ className, ...props }: WithClass<ComponentProps<typeof BaseCollapsible.Root>>) {
  return <BaseCollapsible.Root className={cx("flex w-full flex-col gap-2", className)} {...props} />;
}

// A secondary Button with a chevron that turns down while the panel is open.
export function CollapsibleTrigger({
  children,
  className,
  ...props
}: Omit<ComponentProps<typeof BaseCollapsible.Trigger>, "render" | "className"> & { className?: string }) {
  return (
    <BaseCollapsible.Trigger render={<Button variant="secondary" className={cx("self-start", className)} />} {...props}>
      {children}
      <ChevronRight
        aria-hidden
        className="transition-transform duration-150 [[data-panel-open]>&]:rotate-90 motion-reduce:transition-none"
      />
    </BaseCollapsible.Trigger>
  );
}

export function CollapsiblePanel({ className, ...props }: WithClass<ComponentProps<typeof BaseCollapsible.Panel>>) {
  return (
    <BaseCollapsible.Panel
      className={cx(
        "h-[var(--collapsible-panel-height)] overflow-hidden text-base transition-[height] duration-150 ease-out data-[ending-style]:h-0 data-[starting-style]:h-0 [&[hidden]:not([hidden='until-found'])]:hidden motion-reduce:transition-none",
        className,
      )}
      {...props}
    />
  );
}

Props

PropTypeDefaultWhat it does
defaultOpenbooleanfalseStarts the section open, when it manages its own state.
openboolean—Whether the section is open, when you control it yourself.
onOpenChangefunction—Called with the new open state.
disabledbooleanfalseStops the button from opening or closing the section.

Every other prop goes to the Base UI Collapsible underneath. CollapsibleTrigger takes its label as children.

The thinking

Not everything deserves space on the page at all times. A collapsible is the smallest way to tuck something away: one button, one panel.

  • Built on Base UI: Base UI links the button to its panel, tracks the open state and measures the panel so it can animate to its real height.
  • Three parts: Collapsible holds the state, CollapsibleTrigger is the button and CollapsiblePanel is what opens.
  • Collapsible or Accordion: Use a Collapsible for one section. Use an Accordion for a stack of related sections that belong together.

Interface design

  • A real Button: The trigger is the secondary Button, so it has the same hover, focus and press as every other button.
  • Chevron after the label: The chevron sits after the text, like icons in the other buttons, and turns a quarter turn down while the section is open.
  • No box: The panel has no border or fill. What you put inside decides how it looks.

Interaction

  • Open and close: Click the button, or press Enter or Space when it has focus.
  • Press: The button shrinks slightly while pressed, like every Button.
  • Motion: The panel grows and shrinks in 150ms. With reduced motion turned on it opens at once.

Empathy

  • State is announced: The button reports whether the section is expanded and which panel it controls.
  • Closed means hidden: A closed panel is hidden from screen readers and the tab order, so its links and buttons can't be reached by accident.
  • Name it by what is inside: The label is all anyone sees while it is closed, so Recovery keys is better than Show more.