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 { ActivityOutline } from "@makeplane/propel/icons";

export default function BasicDemo() {
  return (
    <Collapsible
      trigger="Collapsible header"
      icon={<Icon icon={ActivityOutline} 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). Pass trailing for a header-end control (typically an IconButton) — it renders as a sibling of the trigger, not inside it. 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).

Use placement="sidebar" for navigation sections: compact header insets, a rounded full-row hover background, unpadded panel content, and an end-aligned chevron after the optional trailing action. The action remains outside the disclosure button, so it does not toggle the section. The default placement="inline" is unchanged. Pass hug on inline (or any placement) to drop panel padding independently of placement.

import { Collapsible } from "@makeplane/propel/components/collapsible";
import { Icon } from "@makeplane/propel/components/icon";
import { ActivityOutline } from "@makeplane/propel/icons";

export default function BasicDemo() {
  return (
    <Collapsible
      trigger="Collapsible header"
      icon={<Icon icon={ActivityOutline} 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,
  CollapsibleHeader,
  CollapsibleTrigger,
  CollapsibleTriggerTitle,
  CollapsibleTriggerTrailing,
  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={…} trailing={…}>
  …
</Collapsible>

// Atomic parts — graft Base UI yourself
<BaseCollapsible.Root render={<CollapsibleFrame />}>
  <CollapsibleHeader>
    <BaseCollapsible.Trigger render={<CollapsibleTrigger />}>
      {icon}
      <CollapsibleTriggerTitle>…</CollapsibleTriggerTitle>
      {/* Rotating chevron — the shared caret is internal; supply your own */}
    </BaseCollapsible.Trigger>
    <CollapsibleTriggerTrailing>{/* IconButton, never nested in the trigger */}</CollapsibleTriggerTrailing>
  </CollapsibleHeader>
  <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 header is a flex row: the trigger packs [icon?][title][chevron] as a left-aligned cluster and grows so leftover space stays the toggle hit target; CollapsibleTriggerTrailing pins a sibling control at the inline-end. Long titles wrap instead of staying single-line (Figma’s short sample uses nowrap). Panel padding and body type live on CollapsiblePanelContent (px-3 pb-3 for inline, p-0 for sidebar or whenever hug is set, + 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 ChevronDownOutline from @makeplane/propel/icons 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 { ActivityOutline } from "@makeplane/propel/icons";

export default function DefaultOpenDemo() {
  return (
    <Collapsible
      trigger="Collapsible header"
      icon={<Icon icon={ActivityOutline} 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 { ActivityOutline } from "@makeplane/propel/icons";
import * as React from "react";

export default function ControlledDemo() {
  const [open, setOpen] = React.useState(false);
  return (
    <Collapsible
      trigger="Collapsible header"
      icon={<Icon icon={ActivityOutline} 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 { ActivityOutline } from "@makeplane/propel/icons";

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

Trailing action

Pass trailing for a header-end control. It is a sibling of the trigger — never nest a button inside the trigger. Clicking it does not toggle the panel. Root disabled does not disable this control; disable the control itself if both should refuse input.

Screen Recording 2026-08-28 at 7.20.21PM.mov
Show code
import { Collapsible } from "@makeplane/propel/components/collapsible";
import { Icon } from "@makeplane/propel/components/icon";
import { IconButton } from "@makeplane/propel/components/icon-button";
import { AddOutline } from "@makeplane/propel/icons";

export default function TrailingActionDemo() {
  return (
    <Collapsible
      trigger="Attachments 1"
      defaultOpen
      trailing={
        <IconButton
          variant="ghost"
          size="sm"
          aria-label="Add attachment"
          icon={<Icon icon={AddOutline} />}
        />
      }
    >
      Screen Recording 2026-08-28 at 7.20.21PM.mov
    </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 { ActivityOutline } from "@makeplane/propel/icons";

export default function DisabledDemo() {
  return (
    <Collapsible
      trigger="Collapsible header"
      icon={<Icon icon={ActivityOutline} 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 { ActivityOutline } from "@makeplane/propel/icons";

export default function KeepMountedDemo() {
  return (
    <Collapsible
      trigger="Collapsible header"
      icon={<Icon icon={ActivityOutline} 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.
  • A trailing control is a sibling of the trigger, never nested inside it (nested buttons are invalid). It is its own tab stop and does not toggle the panel.
  • Disabled state dims the trigger and blocks open/close via Root disabled (data-disabled / aria-disabled) and swaps the cursor; the trigger stays focusable but will not toggle. Trailing controls are not disabled by Root disabled.
  • 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 and trailing for a header-end control (sibling of the trigger). Forward defaultOpen (uncontrolled) or open + onOpenChange (controlled) to drive it. Set indicator={false} to omit the rotating chevron from the trigger.

PropTypeDefaultDescription
trigger(required)ReactNode—The button content that opens and closes the panel.
childrenReactNode—The 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.
iconReactNode—Element rendered before the label (inline-start), e.g. <Icon icon={Folder} tint="secondary" size="inherit" />.
trailingReactNode—Control rendered at the header's inline-end, as a sibling of the trigger — never inside it (the trigger is a button; nested buttons are invalid). Typical content is an IconButton. Independent of open state: clicking it does not toggle the panel. Root disabled does not disable this control.
placement"inline" | "sidebar"inlineSidebar placement uses compact insets, an unpadded body, and an independent action before the end-aligned indicator. Inline placement keeps the label and indicator together.
indicatorbooleantrueWhether to show the rotating chevron indicator after the trigger label.
keepMountedboolean—Keep the panel in the DOM while closed — Base UI's Panel.keepMounted.
hiddenUntilFoundboolean—Reveal on the browser's find-in-page — Base UI's Panel.hiddenUntilFound.
hugbooleanfalseDrop the content region's own padding, for content that brings its own spacing. Independent of placement — placement="sidebar" already defaults to unpadded content; hug lets inline (or a future placement) opt into the same look without switching placement.