Photon Design System

Collapsible

Shows or hides a single region of content with a smooth height animation. Built on Base UI Collapsible. For several sections where one expands at a time, reach for Accordion.

Import

import { Collapsible } from "@photon-ai/pho-ui/components/collapsible";

Basic

Collapsible.Trigger toggles Collapsible.Panel. Any <svg> in the trigger rotates when open. Left uncontrolled, the panel starts closed unless you pass defaultOpen.

Deployed photon-web to production · 2h ago

Merged #482 “Fix avatar fallback” · 5h ago

Opened #486 “Add Slider component” · 6h ago

import { IconChevronDown } from "@tabler/icons-react";

<Collapsible.Root defaultOpen>
  <Collapsible.Trigger>
    Recent activity
    <IconChevronDown />
  </Collapsible.Trigger>
  <Collapsible.Panel>
    <div className="pt-3">…</div>
  </Collapsible.Panel>
</Collapsible.Root>;

Controlled

Drive the open state yourself with open + onOpenChange when another control needs to read or set it. Omit both to stay uncontrolled and let the trigger manage it.

Panel is closed.

const [open, setOpen] = useState(false);

<Collapsible.Root open={open} onOpenChange={setOpen}>
  <Collapsible.Trigger>
    Environment variables
    <IconChevronDown />
  </Collapsible.Trigger>
  <Collapsible.Panel>
    <div className="pt-3">…</div>
  </Collapsible.Panel>
</Collapsible.Root>;

Anatomy

  • Collapsible.Root — owns the open state; renders a <div>
  • Collapsible.Trigger — the <button> that toggles the panel; gets data-panel-open when open
  • Collapsible.Panel — animates height via --collapsible-panel-height

Props

  • Collapsible.Root
    • open — boolean — controlled open state; pair with onOpenChange
    • defaultOpen — boolean (default false) — initial open state when uncontrolled
    • onOpenChange — (open: boolean, details) => void — fires when the panel opens or closes
    • disabled — boolean (default false) — ignore user interaction
  • Collapsible.Trigger — a native <button>; accepts all <button> attributes
  • Collapsible.Panel
    • keepMounted — boolean (default false) — keep content in the DOM while closed
    • hiddenUntilFound — boolean (default false) — hide with hidden="until-found" so browser find-in-page can reveal it (implies keepMounted)

All three parts also forward className and their native element attributes.

Accessibility

  • Semantics — Collapsible.Trigger is a native <button> with aria-expanded reflecting the open state and aria-controls pointing at the panel while it’s open. A closed panel is removed from the DOM (and the accessibility tree), unless you opt into keepMounted / hiddenUntilFound — then it stays mounted and is hidden with the hidden attribute instead.
  • Focus — focus stays on the trigger through the toggle; it never jumps into the panel. The trigger shows the brand ring on focus-visible only.
  • Find in page — set hiddenUntilFound on the panel so the browser’s in-page search can match hidden text and auto-expand the panel to reveal it.
  • Labelling — give the trigger a clear text label naming what it reveals; the rotating chevron is decorative and carries no state on its own.

Keyboard:

  • Tab — move focus to the trigger
  • Enter / Space — toggle the panel

Best practices

  • Use a collapsible to tuck away secondary detail (activity, advanced options) without leaving the page. Don’t hide anything essential to the main task.
  • Keep the trigger label descriptive of what’s inside, and let the chevron show state — don’t rename the trigger between open and closed.
  • Stay uncontrolled (defaultOpen) unless another control needs the state; reach for open / onOpenChange only when you truly need to drive it.
  • Pad panel content with an inner wrapper (pt-3) rather than the panel itself, so the height animation stays smooth.
  • When several collapsibles are mutually exclusive, switch to Accordion.