Propel

Dialog

A modal overlay for focused tasks and confirmations.

Basic

Show code
import { Button } from "@makeplane/propel/components/button";
import {
  Dialog,
  DialogActions,
  DialogBody,
  DialogClose,
  DialogContent,
  DialogDescription,
  DialogHeader,
  DialogHeading,
  DialogTitle,
  DialogTrigger,
} from "@makeplane/propel/components/dialog";
import { Icon } from "@makeplane/propel/components/icon";
import { IconButton } from "@makeplane/propel/components/icon-button";
import { X } from "lucide-react";

export default function BasicDemo() {
  return (
    <Dialog>
      <Button
        sizing="hug"
        prominence="secondary"
        tone="neutral"
        magnitude="xl"
        render={<DialogTrigger />}
        label="Delete project"
      />
      <DialogContent magnitude="sm">
        <DialogHeader>
          <DialogHeading>
            <DialogTitle>Delete project</DialogTitle>
          </DialogHeading>
          <IconButton
            prominence="ghost"
            tone="neutral"
            magnitude="lg"
            aria-label="Close"
            render={<DialogClose />}
            icon={<Icon icon={X} />}
          />
        </DialogHeader>
        <DialogBody>
          <DialogDescription>
            This permanently removes the project and all of its work items. This action can&apos;t
            be undone.
          </DialogDescription>
        </DialogBody>
        <DialogActions>
          <Button
            sizing="hug"
            prominence="secondary"
            tone="neutral"
            magnitude="xl"
            render={<DialogClose />}
            label="Cancel"
          />
          <Button
            sizing="hug"
            prominence="primary"
            tone="danger"
            magnitude="xl"
            render={<DialogClose />}
            label="Delete"
          />
        </DialogActions>
      </DialogContent>
    </Dialog>
  );
}

Non-dismissable

With disablePointerDismissal on the root, a backdrop click no longer closes the dialog — it dismisses only via an explicit DialogClose or Escape.

Show code
import { Button } from "@makeplane/propel/components/button";
import {
  Dialog,
  DialogActions,
  DialogBody,
  DialogClose,
  DialogContent,
  DialogDescription,
  DialogHeader,
  DialogHeading,
  DialogTitle,
  DialogTrigger,
} from "@makeplane/propel/components/dialog";

export default function NonDismissableDemo() {
  return (
    <Dialog disablePointerDismissal>
      <Button
        sizing="hug"
        prominence="secondary"
        tone="neutral"
        magnitude="xl"
        render={<DialogTrigger />}
        label="Open locked dialog"
      />
      <DialogContent magnitude="sm">
        <DialogHeader>
          <DialogHeading>
            <DialogTitle>Unsaved changes</DialogTitle>
          </DialogHeading>
        </DialogHeader>
        <DialogBody>
          <DialogDescription>
            Choose an action — clicking outside won&apos;t dismiss.
          </DialogDescription>
        </DialogBody>
        <DialogActions>
          <Button
            sizing="hug"
            prominence="secondary"
            tone="neutral"
            magnitude="xl"
            render={<DialogClose />}
            label="Discard"
          />
        </DialogActions>
      </DialogContent>
    </Dialog>
  );
}

Nested dialogs

A DialogTrigger inside a dialog stacks a second dialog above it. Escape unwinds one level at a time.

Show code
import { Button } from "@makeplane/propel/components/button";
import {
  Dialog,
  DialogActions,
  DialogBody,
  DialogClose,
  DialogContent,
  DialogDescription,
  DialogHeader,
  DialogHeading,
  DialogTitle,
  DialogTrigger,
} from "@makeplane/propel/components/dialog";
import { Icon } from "@makeplane/propel/components/icon";
import { IconButton } from "@makeplane/propel/components/icon-button";
import { X } from "lucide-react";

export default function NestedDialogsDemo() {
  return (
    <Dialog>
      <Button
        sizing="hug"
        prominence="secondary"
        tone="neutral"
        magnitude="xl"
        render={<DialogTrigger />}
        label="Invite teammates"
      />
      <DialogContent magnitude="md">
        <DialogHeader>
          <DialogHeading>
            <DialogTitle>Invite teammates</DialogTitle>
          </DialogHeading>
          <IconButton
            prominence="ghost"
            tone="neutral"
            magnitude="lg"
            aria-label="Close"
            render={<DialogClose />}
            icon={<Icon icon={X} />}
          />
        </DialogHeader>
        <DialogBody>
          <DialogDescription>
            Invited members join with the Member role. Need something more specific? Create a custom
            role first.
          </DialogDescription>
        </DialogBody>
        <DialogActions>
          <Dialog>
            <Button
              sizing="hug"
              prominence="secondary"
              tone="neutral"
              magnitude="xl"
              render={<DialogTrigger />}
              label="Create custom role"
            />
            <DialogContent magnitude="sm">
              <DialogHeader>
                <DialogHeading>
                  <DialogTitle>Create custom role</DialogTitle>
                </DialogHeading>
              </DialogHeader>
              <DialogBody>
                <DialogDescription>
                  Custom roles scope what invited members can see and edit.
                </DialogDescription>
              </DialogBody>
              <DialogActions>
                <Button
                  sizing="hug"
                  prominence="secondary"
                  tone="neutral"
                  magnitude="xl"
                  render={<DialogClose />}
                  label="Back"
                />
              </DialogActions>
            </DialogContent>
          </Dialog>
          <Button
            sizing="hug"
            prominence="primary"
            tone="neutral"
            magnitude="xl"
            render={<DialogClose />}
            label="Send invites"
          />
        </DialogActions>
      </DialogContent>
    </Dialog>
  );
}

Open from a menu

Choosing a menu item closes the menu, so the dialog lives outside it and is opened imperatively via controlled open / onOpenChange.

Show code
import { Button } from "@makeplane/propel/components/button";
import {
  Dialog,
  DialogActions,
  DialogBody,
  DialogClose,
  DialogContent,
  DialogDescription,
  DialogHeader,
  DialogHeading,
  DialogTitle,
} from "@makeplane/propel/components/dialog";
import { Icon } from "@makeplane/propel/components/icon";
import { IconButton } from "@makeplane/propel/components/icon-button";
import {
  Menu,
  MenuContent,
  MenuItem,
  MenuSeparator,
  MenuTrigger,
} from "@makeplane/propel/components/menu";
import { Link2, Pencil, Trash2, X } from "lucide-react";
import * as React from "react";

export default function OpenFromMenuDemo() {
  const [open, setOpen] = React.useState(false);
  return (
    <>
      <Menu>
        <Button
          sizing="hug"
          prominence="secondary"
          tone="neutral"
          magnitude="xl"
          render={<MenuTrigger />}
          label="Project options"
        />
        <MenuContent sizing="sm">
          <MenuItem icon={<Icon icon={Pencil} tint="secondary" />} label="Edit" />
          <MenuItem icon={<Icon icon={Link2} tint="secondary" />} label="Copy link" />
          <MenuSeparator />
          <MenuItem
            tone="danger"
            icon={<Icon icon={Trash2} />}
            onClick={() => setOpen(true)}
            label="Delete project…"
          />
        </MenuContent>
      </Menu>
      <Dialog open={open} onOpenChange={setOpen}>
        <DialogContent magnitude="sm">
          <DialogHeader>
            <DialogHeading>
              <DialogTitle>Delete project</DialogTitle>
            </DialogHeading>
            <IconButton
              prominence="ghost"
              tone="neutral"
              magnitude="lg"
              aria-label="Close"
              render={<DialogClose />}
              icon={<Icon icon={X} />}
            />
          </DialogHeader>
          <DialogBody>
            <DialogDescription>
              This permanently removes the project and all of its work items. This action can&apos;t
              be undone.
            </DialogDescription>
          </DialogBody>
          <DialogActions>
            <Button
              sizing="hug"
              prominence="secondary"
              tone="neutral"
              magnitude="xl"
              render={<DialogClose />}
              label="Cancel"
            />
            <Button
              sizing="hug"
              prominence="primary"
              tone="danger"
              magnitude="xl"
              render={<DialogClose />}
              label="Delete"
            />
          </DialogActions>
        </DialogContent>
      </Dialog>
    </>
  );
}

Detached triggers

createDialogHandle() links triggers to a Dialog defined elsewhere in the tree, so several launch points share one dialog without lifting state.

Show code
import { Button } from "@makeplane/propel/components/button";
import {
  createDialogHandle,
  Dialog,
  DialogActions,
  DialogBody,
  DialogClose,
  DialogContent,
  DialogDescription,
  DialogHeader,
  DialogHeading,
  DialogTitle,
  DialogTrigger,
} from "@makeplane/propel/components/dialog";
import { Icon } from "@makeplane/propel/components/icon";
import { IconButton } from "@makeplane/propel/components/icon-button";
import { X } from "lucide-react";

// A handle links triggers that live far from the dialog they open, so several
// launch points can share one dialog without hoisting controlled state.
const shortcutsDialog = createDialogHandle();

export default function DetachedTriggersDemo() {
  return (
    <div className="flex items-center gap-2">
      <Button
        sizing="hug"
        prominence="secondary"
        tone="neutral"
        magnitude="xl"
        render={<DialogTrigger handle={shortcutsDialog} />}
        label="Keyboard shortcuts"
      />
      <Button
        sizing="hug"
        prominence="ghost"
        tone="neutral"
        magnitude="xl"
        render={<DialogTrigger handle={shortcutsDialog} />}
        label="Help"
      />
      <Dialog handle={shortcutsDialog}>
        <DialogContent magnitude="sm">
          <DialogHeader>
            <DialogHeading>
              <DialogTitle>Keyboard shortcuts</DialogTitle>
            </DialogHeading>
            <IconButton
              prominence="ghost"
              tone="neutral"
              magnitude="lg"
              aria-label="Close"
              render={<DialogClose />}
              icon={<Icon icon={X} />}
            />
          </DialogHeader>
          <DialogBody>
            <DialogDescription>
              Press <kbd>C</kbd> to create a work item, <kbd>/</kbd> to search, and <kbd>?</kbd> to
              reopen this list from anywhere.
            </DialogDescription>
          </DialogBody>
          <DialogActions>
            <Button
              sizing="hug"
              prominence="primary"
              tone="neutral"
              magnitude="xl"
              render={<DialogClose />}
              label="Done"
            />
          </DialogActions>
        </DialogContent>
      </Dialog>
    </div>
  );
}

Installation

import { Dialog, DialogTrigger, DialogContent } from "@makeplane/propel/components/dialog";

Props

Dialog

PropTypeRequiredDescription
childrenReactNode | PayloadChildRenderFunction<unknown>NoThe content of the dialog. This can be a regular React node or a render function that receives the `payload` of the active trigger.

DialogContent

PropTypeRequiredDescription
renderReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, {}>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.
magnitude"sm" | "md" | "lg"Yes