Propel

Menu

A popup list of actions or options opened from a trigger.

Basic

Show code
import { Button } from "@makeplane/propel/components/button";
import { Icon } from "@makeplane/propel/components/icon";
import {
  Menu,
  MenuContent,
  MenuItem,
  MenuSeparator,
  MenuTrigger,
} from "@makeplane/propel/components/menu";
import { Copy, ExternalLink, Pencil, Trash2 } from "lucide-react";

export default function BasicDemo() {
  return (
    <Menu>
      <Button
        sizing="hug"
        prominence="secondary"
        tone="neutral"
        magnitude="md"
        render={<MenuTrigger />}
        label="Actions"
      />
      <MenuContent sizing="sm">
        <MenuItem icon={<Icon icon={Pencil} tint="secondary" />} label="Edit" />
        <MenuItem icon={<Icon icon={Copy} tint="secondary" />} label="Make a copy" />
        <MenuItem icon={<Icon icon={ExternalLink} tint="secondary" />} label="Open in new tab" />
        <MenuSeparator />
        <MenuItem tone="danger" icon={<Icon icon={Trash2} />} label="Delete" />
      </MenuContent>
    </Menu>
  );
}

Checkbox items

Multi-select rows built from MenuCheckboxItem. Each row keeps its own checked state and the menu stays open on click.

Show code
import { Button } from "@makeplane/propel/components/button";
import {
  Menu,
  MenuCheckboxItem,
  MenuContent,
  MenuSearch,
  MenuTrigger,
} from "@makeplane/propel/components/menu";
import * as React from "react";

const PROPERTIES = [
  { key: "assignee", label: "Assignee" },
  { key: "priority", label: "Priority" },
  { key: "due_date", label: "Due date" },
  { key: "labels", label: "Labels" },
  { key: "start_date", label: "Start date" },
] as const;

export default function CheckboxItemsDemo() {
  const [checked, setChecked] = React.useState<Record<string, boolean>>({
    assignee: true,
    priority: true,
  });
  const [query, setQuery] = React.useState("");
  const visible = PROPERTIES.filter((p) => p.label.toLowerCase().includes(query.toLowerCase()));
  return (
    <Menu>
      <Button
        sizing="hug"
        prominence="secondary"
        tone="neutral"
        magnitude="md"
        render={<MenuTrigger />}
        label="Display properties"
      />
      <MenuContent
        sizing="sm"
        search={<MenuSearch value={query} onValueChange={setQuery} placeholder="Search" />}
      >
        {visible.map((p) => (
          <MenuCheckboxItem
            key={p.key}
            checked={Boolean(checked[p.key])}
            onCheckedChange={(next) => setChecked((c) => ({ ...c, [p.key]: next }))}
            label={p.label}
          />
        ))}
      </MenuContent>
    </Menu>
  );
}

Radio group

Single-select rows wrapped in a MenuRadioGroup sharing one value.

Show code
import { Button } from "@makeplane/propel/components/button";
import {
  Menu,
  MenuContent,
  MenuRadioGroup,
  MenuRadioItem,
  MenuTrigger,
} from "@makeplane/propel/components/menu";
import * as React from "react";

export default function RadioGroupDemo() {
  const [density, setDensity] = React.useState("comfortable");
  return (
    <Menu>
      <Button
        sizing="hug"
        prominence="secondary"
        tone="neutral"
        magnitude="md"
        render={<MenuTrigger />}
        label="Density"
      />
      <MenuContent sizing="sm">
        <MenuRadioGroup value={density} onValueChange={setDensity}>
          <MenuRadioItem value="comfortable" closeOnClick={false} label="Comfortable" />
          <MenuRadioItem value="compact" closeOnClick={false} label="Compact" />
        </MenuRadioGroup>
      </MenuContent>
    </Menu>
  );
}

With description

A description gives each row a muted second line; passing it switches the row to a taller, top-aligned layout.

Show code
import { Button } from "@makeplane/propel/components/button";
import { Icon } from "@makeplane/propel/components/icon";
import { Menu, MenuContent, MenuItem, MenuTrigger } from "@makeplane/propel/components/menu";
import { Globe, Lock } from "lucide-react";
import * as React from "react";

export default function WithDescriptionDemo() {
  const [selected, setSelected] = React.useState("private");
  return (
    <Menu>
      <Button
        sizing="hug"
        prominence="secondary"
        tone="neutral"
        magnitude="md"
        render={<MenuTrigger />}
        label="Visibility"
      />
      <MenuContent sizing="lg">
        <MenuItem
          icon={<Icon icon={Lock} tint="secondary" />}
          description="Accessible only by invite"
          selected={selected === "private"}
          closeOnClick={false}
          onClick={() => setSelected("private")}
          label="Private"
        />
        <MenuItem
          icon={<Icon icon={Globe} tint="secondary" />}
          description="Anyone in the workspace except Guests can join"
          selected={selected === "public"}
          closeOnClick={false}
          onClick={() => setSelected("public")}
          label="Public"
        />
      </MenuContent>
    </Menu>
  );
}

MenuSubmenu nests a MenuSubmenuContent behind a MenuSubmenuTrigger row.

Show code
import { Badge } from "@makeplane/propel/components/badge";
import { Button } from "@makeplane/propel/components/button";
import { Icon } from "@makeplane/propel/components/icon";
import {
  Menu,
  MenuContent,
  MenuItem,
  MenuSubmenu,
  MenuSubmenuContent,
  MenuSubmenuTrigger,
  MenuTrigger,
} from "@makeplane/propel/components/menu";
import {
  Circle,
  CircleCheck,
  CircleDashed,
  CircleDot,
  CircleX,
  SignalHigh,
  SignalLow,
  SignalMedium,
} from "lucide-react";

export default function SubmenuDemo() {
  return (
    <Menu>
      <Button
        sizing="hug"
        prominence="secondary"
        tone="neutral"
        magnitude="md"
        render={<MenuTrigger />}
        label="Filter by"
      />
      <MenuContent sizing="sm">
        <MenuSubmenu>
          <MenuSubmenuTrigger
            endContent={<Badge magnitude="sm" tone="neutral" label="4" />}
            label="Priority"
          />
          <MenuSubmenuContent sizing="sm">
            <MenuItem
              icon={<Icon icon={SignalHigh} tint="secondary" />}
              closeOnClick={false}
              label="Urgent"
            />
            <MenuItem
              icon={<Icon icon={SignalHigh} tint="secondary" />}
              closeOnClick={false}
              label="High"
            />
            <MenuItem
              icon={<Icon icon={SignalMedium} tint="secondary" />}
              closeOnClick={false}
              label="Medium"
            />
            <MenuItem
              icon={<Icon icon={SignalLow} tint="secondary" />}
              closeOnClick={false}
              label="Low"
            />
          </MenuSubmenuContent>
        </MenuSubmenu>
        <MenuSubmenu>
          <MenuSubmenuTrigger
            endContent={<Badge magnitude="sm" tone="neutral" label="5" />}
            label="State"
          />
          <MenuSubmenuContent sizing="sm">
            <MenuItem
              icon={<Icon icon={CircleDashed} tint="secondary" />}
              closeOnClick={false}
              label="Backlog"
            />
            <MenuItem
              icon={<Icon icon={Circle} tint="secondary" />}
              closeOnClick={false}
              label="Todo"
            />
            <MenuItem
              icon={<Icon icon={CircleDot} tint="secondary" />}
              closeOnClick={false}
              label="In progress"
            />
            <MenuItem
              icon={<Icon icon={CircleCheck} tint="secondary" />}
              closeOnClick={false}
              label="Done"
            />
            <MenuItem
              icon={<Icon icon={CircleX} tint="secondary" />}
              closeOnClick={false}
              label="Cancelled"
            />
          </MenuSubmenuContent>
        </MenuSubmenu>
      </MenuContent>
    </Menu>
  );
}

Installation

import {
  Menu,
  MenuContent,
  MenuItem,
  MenuSeparator,
  MenuTrigger,
} from "@makeplane/propel/components/menu";

Props

Menu

PropTypeRequiredDescription
openbooleanNoWhether the menu is open. Controlled; pair with `onOpenChange`.
defaultOpenbooleanNoWhether the menu is open on mount. Uncontrolled.
onOpenChange((open: boolean, eventDetails: MenuRootChangeEventDetails) => void)NoCalled with the next open state when the menu opens or closes.
modalbooleanNoModal behavior while open.
childrenReactNode | PayloadChildRenderFunction<Payload>NoThe trigger and menu surface (`MenuTrigger`, `MenuContent`).

MenuContent

PropTypeRequiredDescription
renderReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, MenuPopupState>NoAllows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a `ReactElement` or a function that returns the element to render.
side"top" | "bottom" | "left" | "right" | "inline-end" | "inline-start"NoWhich side of the trigger the menu opens toward.
sideOffsetnumber | OffsetFunctionNoDistance in px between the trigger and the menu.
align"center" | "start" | "end"NoAlignment of the menu relative to the trigger along `side`.
sizing"auto" | "anchor" | "sm" | "md" | "lg"NoHow the menu popup sizes itself.
searchReactNodeNoSticky chrome pinned above the role="menu" popup.
footerReactNodeNoSticky chrome pinned below the role="menu" popup.

MenuItem

PropTypeRequiredDescription
renderReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, MenuItemState>NoAllows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a `ReactElement` or a function that returns the element to render.
tone"neutral" | "accent" | "danger"NoRow text palette.
iconReactNodeNoLeading element before the label, e.g. `<Icon icon={Settings} tint="secondary" />`.
labelstringYesPrimary row label.
descriptionstringNoMuted secondary line under the label.
secondaryTextstringNoMuted text shown inline after the label.
endContentReactElement<unknown, string | JSXElementConstructor<any>>NoElement rendered at the inline end of the row, e.g. `<Shortcut keys="⌘ K" />`.
selectedbooleanNoSingle-select selected state.