Propel

Drawer

An edge-anchored overlay panel that slides in for details, navigation, and side tasks.

Basic

Show code
import { Button } from "@makeplane/propel/components/button";
import {
  Drawer,
  DrawerBody,
  DrawerClose,
  DrawerDescription,
  DrawerFooter,
  DrawerHeader,
  DrawerHeaderContent,
  DrawerPanel,
  DrawerTitle,
  DrawerTrigger,
} from "@makeplane/propel/components/drawer";
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 (
    <Drawer>
      <DrawerTrigger
        render={
          <Button
            sizing="hug"
            prominence="secondary"
            tone="neutral"
            magnitude="xl"
            label="Open details"
          />
        }
      />
      <DrawerPanel side="end">
        <DrawerHeader>
          <DrawerHeaderContent>
            <DrawerTitle>Work item details</DrawerTitle>
            <DrawerDescription>Edit the fields for this work item.</DrawerDescription>
          </DrawerHeaderContent>
          <DrawerClose
            render={
              <IconButton
                prominence="ghost"
                tone="neutral"
                magnitude="lg"
                aria-label="Close"
                icon={<Icon icon={X} />}
              />
            }
          />
        </DrawerHeader>
        <DrawerBody>Panel body content goes here.</DrawerBody>
        <DrawerFooter>
          <DrawerClose
            render={
              <Button
                sizing="hug"
                prominence="ghost"
                tone="neutral"
                magnitude="lg"
                label="Cancel"
              />
            }
          />
          <Button sizing="hug" prominence="primary" tone="neutral" magnitude="lg" label="Save" />
        </DrawerFooter>
      </DrawerPanel>
    </Drawer>
  );
}

Side

The side prop anchors the panel to the inline-start or inline-end edge; the panel slides in from that edge.

Show code
import { Button } from "@makeplane/propel/components/button";
import {
  Drawer,
  DrawerBody,
  DrawerClose,
  DrawerHeader,
  DrawerHeaderContent,
  DrawerPanel,
  DrawerTitle,
  DrawerTrigger,
} from "@makeplane/propel/components/drawer";
import { Icon } from "@makeplane/propel/components/icon";
import { IconButton } from "@makeplane/propel/components/icon-button";
import { X } from "lucide-react";

export default function StartSideDemo() {
  return (
    <Drawer>
      <DrawerTrigger
        render={
          <Button
            sizing="hug"
            prominence="secondary"
            tone="neutral"
            magnitude="xl"
            label="Open navigation"
          />
        }
      />
      <DrawerPanel side="start">
        <DrawerHeader>
          <DrawerHeaderContent>
            <DrawerTitle>Navigation</DrawerTitle>
          </DrawerHeaderContent>
          <DrawerClose
            render={
              <IconButton
                prominence="ghost"
                tone="neutral"
                magnitude="lg"
                aria-label="Close"
                icon={<Icon icon={X} />}
              />
            }
          />
        </DrawerHeader>
        <DrawerBody>Navigation links go here.</DrawerBody>
      </DrawerPanel>
    </Drawer>
  );
}

Nested

Show code
import { Button } from "@makeplane/propel/components/button";
import {
  Drawer,
  DrawerBody,
  DrawerDescription,
  DrawerHeader,
  DrawerHeaderContent,
  DrawerPanel,
  DrawerTitle,
  DrawerTrigger,
} from "@makeplane/propel/components/drawer";

export default function NestedDemo() {
  return (
    <Drawer>
      <DrawerTrigger
        render={
          <Button
            sizing="hug"
            prominence="secondary"
            tone="neutral"
            magnitude="xl"
            label="Open settings"
          />
        }
      />
      <DrawerPanel side="end">
        <DrawerHeader>
          <DrawerHeaderContent>
            <DrawerTitle>Settings</DrawerTitle>
            <DrawerDescription>Workspace-wide preferences.</DrawerDescription>
          </DrawerHeaderContent>
        </DrawerHeader>
        <DrawerBody>
          <Drawer>
            <DrawerTrigger
              render={
                <Button
                  sizing="hug"
                  prominence="secondary"
                  tone="neutral"
                  magnitude="lg"
                  label="Advanced options"
                />
              }
            />
            <DrawerPanel side="end">
              <DrawerHeader>
                <DrawerHeaderContent>
                  <DrawerTitle>Advanced options</DrawerTitle>
                </DrawerHeaderContent>
              </DrawerHeader>
              <DrawerBody>Nested drawer content.</DrawerBody>
            </DrawerPanel>
          </Drawer>
        </DrawerBody>
      </DrawerPanel>
    </Drawer>
  );
}

Detached triggers

createDrawerHandle() links triggers to a Drawer defined elsewhere in the tree; each trigger passes a payload the drawer renders through function-as-children.

Show code
import { Button } from "@makeplane/propel/components/button";
import {
  createDrawerHandle,
  Drawer,
  DrawerDescription,
  DrawerHeader,
  DrawerHeaderContent,
  DrawerPanel,
  DrawerTitle,
  DrawerTrigger,
} from "@makeplane/propel/components/drawer";

// A handle shared by triggers that live far from the drawer they open — the
// same details panel launched from unrelated corners of the UI, no lifted state.
const detailsDrawer = createDrawerHandle();

export default function DetachedTriggersDemo() {
  return (
    <div className="flex flex-wrap items-center gap-2">
      <DrawerTrigger
        handle={detailsDrawer}
        payload="WEB-101"
        render={
          <Button
            sizing="hug"
            prominence="secondary"
            tone="neutral"
            magnitude="xl"
            label="WEB-101"
          />
        }
      />
      <DrawerTrigger
        handle={detailsDrawer}
        payload="WEB-202"
        render={
          <Button
            sizing="hug"
            prominence="secondary"
            tone="neutral"
            magnitude="xl"
            label="WEB-202"
          />
        }
      />
      <Drawer handle={detailsDrawer}>
        {({ payload }) => (
          <DrawerPanel side="end">
            <DrawerHeader>
              <DrawerHeaderContent>
                <DrawerTitle>{typeof payload === "string" ? payload : "Details"}</DrawerTitle>
                <DrawerDescription>Work item details.</DrawerDescription>
              </DrawerHeaderContent>
            </DrawerHeader>
          </DrawerPanel>
        )}
      </Drawer>
    </div>
  );
}

Controlled

A controlled Drawer can intercept its own dismissal — here a draft raises a confirmation via eventDetails.cancel() before the panel closes.

Show code
import {
  AlertDialog,
  AlertDialogActions,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogHeader,
  AlertDialogIntro,
  AlertDialogTitle,
} from "@makeplane/propel/components/alert-dialog";
import { Button } from "@makeplane/propel/components/button";
import {
  Drawer,
  DrawerBody,
  DrawerClose,
  DrawerDescription,
  DrawerHeader,
  DrawerHeaderContent,
  DrawerPanel,
  DrawerTitle,
  DrawerTrigger,
} from "@makeplane/propel/components/drawer";
import { Icon } from "@makeplane/propel/components/icon";
import { IconButton } from "@makeplane/propel/components/icon-button";
import { TextArea } from "@makeplane/propel/components/text-area";
import { X } from "lucide-react";
import * as React from "react";

export default function ControlledDemo() {
  const [open, setOpen] = React.useState(false);
  const [confirmOpen, setConfirmOpen] = React.useState(false);
  const [draft, setDraft] = React.useState("");
  return (
    <>
      <Drawer
        open={open}
        onOpenChange={(nextOpen, eventDetails) => {
          if (!nextOpen && draft.trim() !== "") {
            // Block the dismissal and ask for confirmation while a draft exists.
            eventDetails.cancel();
            setConfirmOpen(true);
            return;
          }
          setOpen(nextOpen);
        }}
      >
        <DrawerTrigger
          render={
            <Button
              sizing="hug"
              prominence="secondary"
              tone="neutral"
              magnitude="xl"
              label="Add comment"
            />
          }
        />
        <DrawerPanel side="end">
          <DrawerHeader>
            <DrawerHeaderContent>
              <DrawerTitle>New comment</DrawerTitle>
              <DrawerDescription>Share feedback on this work item.</DrawerDescription>
            </DrawerHeaderContent>
            <DrawerClose
              render={
                <IconButton
                  prominence="ghost"
                  tone="neutral"
                  magnitude="lg"
                  aria-label="Close"
                  icon={<Icon icon={X} />}
                />
              }
            />
          </DrawerHeader>
          <DrawerBody>
            <TextArea
              magnitude="lg"
              surface="field"
              resize="none"
              rows={4}
              aria-label="Comment"
              value={draft}
              onChange={(event) => setDraft(event.target.value)}
            />
          </DrawerBody>
        </DrawerPanel>
      </Drawer>
      <AlertDialog open={confirmOpen} onOpenChange={setConfirmOpen}>
        <AlertDialogContent>
          <AlertDialogHeader>
            <AlertDialogIntro>
              <AlertDialogTitle>Discard comment?</AlertDialogTitle>
              <AlertDialogDescription>
                Your draft will be lost if you close the panel now.
              </AlertDialogDescription>
            </AlertDialogIntro>
          </AlertDialogHeader>
          <AlertDialogActions>
            <Button
              sizing="hug"
              prominence="ghost"
              tone="neutral"
              magnitude="lg"
              onClick={() => setConfirmOpen(false)}
              label="Keep editing"
            />
            <Button
              sizing="hug"
              prominence="primary"
              tone="danger"
              magnitude="lg"
              onClick={() => {
                setConfirmOpen(false);
                setOpen(false);
                setDraft("");
              }}
              label="Discard"
            />
          </AlertDialogActions>
        </AlertDialogContent>
      </AlertDialog>
    </>
  );
}

Installation

import { Drawer, DrawerTrigger, DrawerPanel } from "@makeplane/propel/components/drawer";

Props

Drawer

PropTypeRequiredDescription
childrenReactNode | PayloadChildRenderFunction<unknown>NoThe content of the drawer.

DrawerPanel

PropTypeRequiredDescription
side"end" | "start"YesThe viewport edge the drawer is anchored to. "end" pins to the inline-end (right in LTR); "start" pins to the inline-start (left in LTR). The slide animation direction and the leading-edge shadow border both follow this value.
childrenReactNodeNoThe panel body — typically a `DrawerHeader`, `DrawerBody`, and `DrawerFooter`.