Propel

Collapsible

A single show/hide disclosure that expands and collapses a panel of content.

Show code
import { Collapsible } from "@makeplane/propel/components/collapsible";
import { Icon } from "@makeplane/propel/components/icon";
import { Activity } from "lucide-react";

export default function BasicDemo() {
  return (
    <Collapsible
      trigger="Collapsible header"
      icon={<Icon icon={Activity} tint="secondary" size="inherit" />}
    >
      Content that expands and collapses with this section.
    </Collapsible>
  );
}

Installation

import { Collapsible } from "@makeplane/propel/components/collapsible";

Usage

Pass trigger for the header label and children for the panel body. Pass a public <Icon> into icon for the leading 16px glyph (Figma’s resting art includes it). The rotating chevron is on by default (indicator defaults to true). Drive open state with defaultOpen (uncontrolled) or open + onOpenChange (controlled). Pass keepMounted to leave the panel in the DOM while closed, or hiddenUntilFound so closed content also stays findable via the browser’s find-in-page — hiddenUntilFound alone already keeps the panel mounted, so the two are not meant to be combined (Base UI Panel props).

import { Collapsible } from "@makeplane/propel/components/collapsible";
import { Icon } from "@makeplane/propel/components/icon";
import { Activity } from "lucide-react";

export default function BasicDemo() {
  return (
    <Collapsible
      trigger="Collapsible header"
      icon={<Icon icon={Activity} tint="secondary" size="inherit" />}
    >
      Content that expands and collapses with this section.
    </Collapsible>
  );
}

Anatomy

import { Collapsible as BaseCollapsible } from "@base-ui/react/collapsible";
import {
  Collapsible,
  CollapsibleTrigger,
  CollapsibleTriggerTitle,
  CollapsiblePanel,
  CollapsiblePanelContent,
} from "@makeplane/propel/components/collapsible";
// Styled Root frame (`w-full`) — separate from the ready-made `Collapsible` above (name collision).
import { Collapsible as CollapsibleFrame } from "@makeplane/propel/elements/collapsible";

// Ready-made — grafts Base UI Root → CollapsibleFrame for you
<Collapsible trigger="…" icon={…}>

</Collapsible>

// Atomic parts — graft Base UI yourself
<BaseCollapsible.Root render={<CollapsibleFrame />}>
  <BaseCollapsible.Trigger render={<CollapsibleTrigger />}>
    {icon}
    <CollapsibleTriggerTitle>…</CollapsibleTriggerTitle>
    {/* Rotating chevron — the shared caret is internal; supply your own */}
  </BaseCollapsible.Trigger>
  <BaseCollapsible.Panel render={<CollapsiblePanel />}>
    <CollapsiblePanelContent>…</CollapsiblePanelContent>
  </BaseCollapsible.Panel>
</BaseCollapsible.Root>

The Root frame is full width so centered hosts don’t shrink-wrap when closed. The trigger packs [icon?][title][chevron] as a left-aligned cluster. Long titles wrap instead of staying single-line (Figma’s short sample uses nowrap). Panel padding and body type live on CollapsiblePanelContent (px-3 pt-1.5 pb-3 + body-xs secondary) so the height animation on CollapsiblePanel measures cleanly and List sections (children directly in the panel) do not inherit Collapsible prose tokens. The rotating chevron itself is an internal shared primitive (not exported) — an atomic assembly supplies its own, e.g. a lucide-react ChevronDown rotated off data-panel-open.

Examples

Without icon

Omit icon when the row should be label + chevron only.

Show code
import { Collapsible } from "@makeplane/propel/components/collapsible";

export default function WithoutIconDemo() {
  return (
    <Collapsible trigger="Collapsible header">
      Content that expands and collapses with this section.
    </Collapsible>
  );
}

Default open

Content that expands and collapses with this section.
Show code
import { Collapsible } from "@makeplane/propel/components/collapsible";
import { Icon } from "@makeplane/propel/components/icon";
import { Activity } from "lucide-react";

export default function DefaultOpenDemo() {
  return (
    <Collapsible
      trigger="Collapsible header"
      icon={<Icon icon={Activity} tint="secondary" size="inherit" />}
      defaultOpen
    >
      Content that expands and collapses with this section.
    </Collapsible>
  );
}

Controlled

Pass open and onOpenChange to drive the disclosure from your own state instead of defaultOpen.

Show code
import { Collapsible } from "@makeplane/propel/components/collapsible";
import { Icon } from "@makeplane/propel/components/icon";
import { Activity } from "lucide-react";
import * as React from "react";

export default function ControlledDemo() {
  const [open, setOpen] = React.useState(false);
  return (
    <Collapsible
      trigger="Collapsible header"
      icon={<Icon icon={Activity} tint="secondary" size="inherit" />}
      open={open}
      onOpenChange={setOpen}
    >
      Content that expands and collapses with this section.
    </Collapsible>
  );
}

Without indicator

Show code
import { Collapsible } from "@makeplane/propel/components/collapsible";
import { Icon } from "@makeplane/propel/components/icon";
import { Activity } from "lucide-react";

export default function WithoutIndicatorDemo() {
  return (
    <Collapsible
      trigger="Collapsible header"
      icon={<Icon icon={Activity} tint="secondary" size="inherit" />}
      indicator={false}
    >
      Content that expands and collapses with this section.
    </Collapsible>
  );
}

Disabled

Pass Root disabled to dim the trigger and block open/close.

Show code
import { Collapsible } from "@makeplane/propel/components/collapsible";
import { Icon } from "@makeplane/propel/components/icon";
import { Activity } from "lucide-react";

export default function DisabledDemo() {
  return (
    <Collapsible
      trigger="Collapsible header"
      icon={<Icon icon={Activity} tint="secondary" size="inherit" />}
      disabled
    >
      Content that expands and collapses with this section.
    </Collapsible>
  );
}

Keep mounted

Pass keepMounted to leave the panel in the DOM while closed. hiddenUntilFound does the same (plus browser find-in-page) — pass one or the other, not both.

Show code
import { Collapsible } from "@makeplane/propel/components/collapsible";
import { Icon } from "@makeplane/propel/components/icon";
import { Activity } from "lucide-react";

export default function KeepMountedDemo() {
  return (
    <Collapsible
      trigger="Collapsible header"
      icon={<Icon icon={Activity} tint="secondary" size="inherit" />}
      keepMounted
    >
      Content that expands and collapses with this section.
    </Collapsible>
  );
}

Accessibility

  • The trigger is a native <button> with Base UI’s aria-expanded / aria-controls.
  • The chevron is decorative (aria-hidden); the accessible name comes from the trigger label.
  • Disabled state dims the row via Root disabled (data-disabled / aria-disabled) and swaps the cursor; the trigger stays focusable but will not toggle.
  • Focus-visible draws the shared accent ring on the trigger.

API Reference

Collapsible

The ready-made collapsible: a single show/hide disclosure that wires the trigger and panel for the 90% case. Pass `trigger` for the toggle button label and `children` for the body; optional `icon` for a leading glyph. Forward `defaultOpen` (uncontrolled) or `open` + `onOpenChange` (controlled) to drive it. Set `indicator={false}` to omit the rotating chevron from the trigger.

PropTypeDefaultDescription
trigger(required)ReactNodeThe button content that opens and closes the panel.
childrenReactNodeThe collapsible content region.
renderReactElement<unknown, string | JSXElementConstructor<any>>Element to graft the styled frame onto instead of the default `div` — element form only, the callback form's `state` isn't wired through the styled frame.
iconReactNodeElement rendered before the label (inline-start), e.g. `<Icon icon={Folder} tint="secondary" size="inherit" />`.
indicatorbooleantrueWhether to show the rotating chevron indicator after the trigger label.
keepMountedbooleanKeep the panel in the DOM while closed — Base UI's `Panel.keepMounted`.
hiddenUntilFoundbooleanReveal on the browser's find-in-page — Base UI's `Panel.hiddenUntilFound`.