Propel

Drawer

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

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

/**
 * Default use case: `sm` inline flush column absolutely positioned in a `relative` parent —
 * overlays page content without pushing it.
 */
export default function BasicDemo() {
  return (
    <div className="relative h-[28rem] w-full overflow-hidden rounded-xl border border-subtle bg-layer-2">
      <Drawer swipeDirection="right">
        <div className="flex h-full items-center justify-center p-4">
          <DrawerTrigger
            render={<Button stretch="auto" variant="secondary" size="lg" label="Open details" />}
          />
        </div>
        <DrawerPanel variant="inline" side="end" size="sm">
          <DrawerHeader>
            <DrawerHeaderStart>
              <DrawerHeaderIcon>
                <Box />
              </DrawerHeaderIcon>
              <DrawerHeaderContent>
                <DrawerTitle>Page details</DrawerTitle>
                <DrawerDescription>Inline sm absolute column.</DrawerDescription>
              </DrawerHeaderContent>
            </DrawerHeaderStart>
            <DrawerHeaderActions side="end">
              <DrawerClose
                render={
                  <IconButton
                    variant="ghost"
                    size="md"
                    aria-label="Close"
                    icon={<Icon icon={X} />}
                  />
                }
              />
            </DrawerHeaderActions>
          </DrawerHeader>
          <DrawerBody>Sidebar content goes here.</DrawerBody>
          <DrawerFooter>
            <DrawerClose
              render={<Button stretch="auto" variant="ghost" size="md" label="Cancel" />}
            />
            <Button stretch="auto" variant="primary" size="md" label="Save" />
          </DrawerFooter>
        </DrawerPanel>
      </Drawer>
    </div>
  );
}

Installation

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

Usage

import { Button } from "@makeplane/propel/components/button";
import {
  Drawer,
  DrawerBody,
  DrawerClose,
  DrawerDescription,
  DrawerFooter,
  DrawerHeader,
  DrawerHeaderActions,
  DrawerHeaderContent,
  DrawerHeaderIcon,
  DrawerHeaderStart,
  DrawerPanel,
  DrawerTitle,
  DrawerTrigger,
} from "@makeplane/propel/components/drawer";
import { Icon } from "@makeplane/propel/components/icon";
import { IconButton } from "@makeplane/propel/components/icon-button";
import { Box, X } from "lucide-react";

/**
 * Default use case: `sm` inline flush column absolutely positioned in a `relative` parent —
 * overlays page content without pushing it.
 */
export default function BasicDemo() {
  return (
    <div className="relative h-[28rem] w-full overflow-hidden rounded-xl border border-subtle bg-layer-2">
      <Drawer swipeDirection="right">
        <div className="flex h-full items-center justify-center p-4">
          <DrawerTrigger
            render={<Button stretch="auto" variant="secondary" size="lg" label="Open details" />}
          />
        </div>
        <DrawerPanel variant="inline" side="end" size="sm">
          <DrawerHeader>
            <DrawerHeaderStart>
              <DrawerHeaderIcon>
                <Box />
              </DrawerHeaderIcon>
              <DrawerHeaderContent>
                <DrawerTitle>Page details</DrawerTitle>
                <DrawerDescription>Inline sm absolute column.</DrawerDescription>
              </DrawerHeaderContent>
            </DrawerHeaderStart>
            <DrawerHeaderActions side="end">
              <DrawerClose
                render={
                  <IconButton
                    variant="ghost"
                    size="md"
                    aria-label="Close"
                    icon={<Icon icon={X} />}
                  />
                }
              />
            </DrawerHeaderActions>
          </DrawerHeader>
          <DrawerBody>Sidebar content goes here.</DrawerBody>
          <DrawerFooter>
            <DrawerClose
              render={<Button stretch="auto" variant="ghost" size="md" label="Cancel" />}
            />
            <Button stretch="auto" variant="primary" size="md" label="Save" />
          </DrawerFooter>
        </DrawerPanel>
      </Drawer>
    </div>
  );
}

Examples

Use cases map to variant + size. Prefer backdrop off in demos (see Backdrop for the opt-in). Prefer sm + inline (absolute flush column in a relative parent — does not push siblings) unless the example is deliberately about a floating card in a parent shell (Contained).

Use case variant size Placement
Default sidebar inline sm Absolute in relative parent (no push)
Floating details overlay md / lg document.body (inset card)
Under app chrome contained md / lg Positioned parent container

Overlay

Floating card over the app (document.body). No backdrop.

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

/** Floating card on `document.body` — `md` / `lg`, backdrop off. */
export default function OverlayDemo() {
  return (
    <Drawer swipeDirection="right">
      <DrawerTrigger
        render={<Button stretch="auto" variant="secondary" size="lg" label="Open overlay" />}
      />
      <DrawerPanel variant="overlay" side="end" size="md" aria-label="Work item details">
        <DrawerHeader>
          <DrawerHeaderActions side="start">
            <IconButton
              variant="ghost"
              size="md"
              aria-label="Back"
              icon={<Icon icon={ArrowLeft} />}
            />
            <IconButton
              variant="ghost"
              size="md"
              aria-label="Expand"
              icon={<Icon icon={Maximize2} />}
            />
            <IconButton
              variant="ghost"
              size="md"
              aria-label="More"
              icon={<Icon icon={MoreHorizontal} />}
            />
          </DrawerHeaderActions>
          <DrawerHeaderActions side="end">
            <IconButton
              variant="ghost"
              size="md"
              aria-label="More actions"
              icon={<Icon icon={MoreHorizontal} />}
            />
            <DrawerClose
              render={
                <IconButton variant="ghost" size="md" aria-label="Close" icon={<Icon icon={X} />} />
              }
            />
          </DrawerHeaderActions>
        </DrawerHeader>
        <DrawerBody>Panel body content goes here.</DrawerBody>
        <DrawerFooter>
          <DrawerClose
            render={<Button stretch="auto" variant="ghost" size="md" label="Cancel" />}
          />
          <Button stretch="auto" variant="secondary" size="md" label="Secondary" />
          <Button stretch="auto" variant="primary" size="md" label="Save" />
        </DrawerFooter>
      </DrawerPanel>
    </Drawer>
  );
}

Contained

Floating card portaled into a positioned parent (e.g. main shell under a search topnav). Does not cover chrome outside that shell.

Topnav / search

Panel portals into this parent shell only — below the topnav, no page scrim.

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

/** Floating card portaled into a parent shell (under topnav) — not the page body. */
export default function ContainedDemo() {
  const shellRef = React.useRef<HTMLDivElement>(null);
  return (
    <div className="flex h-[28rem] w-full flex-col overflow-hidden rounded-xl border border-subtle">
      <div className="flex h-11 shrink-0 items-center border-b border-subtle px-4 text-body-sm-medium text-secondary">
        Topnav / search
      </div>
      <div ref={shellRef} className="relative min-h-0 flex-1 overflow-hidden bg-layer-2">
        <Drawer swipeDirection="right">
          <div className="flex flex-col gap-3 p-4">
            <DrawerTrigger
              render={<Button stretch="auto" variant="secondary" size="lg" label="Open panel" />}
            />
            <p className="text-body-sm-regular text-secondary">
              Panel portals into this parent shell only — below the topnav, no page scrim.
            </p>
          </div>
          <DrawerPanel
            variant="contained"
            container={shellRef}
            side="end"
            size="md"
            aria-label="Work item details"
          >
            <DrawerHeader>
              <DrawerHeaderActions side="start">
                <IconButton
                  variant="ghost"
                  size="md"
                  aria-label="Back"
                  icon={<Icon icon={ArrowLeft} />}
                />
              </DrawerHeaderActions>
              <DrawerHeaderActions side="end">
                <DrawerClose
                  render={
                    <IconButton
                      variant="ghost"
                      size="md"
                      aria-label="Close"
                      icon={<Icon icon={X} />}
                    />
                  }
                />
              </DrawerHeaderActions>
            </DrawerHeader>
            <DrawerBody>Panel body content goes here.</DrawerBody>
          </DrawerPanel>
        </Drawer>
      </div>
    </div>
  );
}

Side

side anchors the panel to the inline-start or inline-end edge. sm stays an absolute flush column inside the relative parent.

Pair Drawer swipeDirection with side so swipe-to-dismiss matches the edge (Base UI defaults to "down"):

side swipeDirection
end right
start left
Show code
import { Button } from "@makeplane/propel/components/button";
import {
  Drawer,
  DrawerBody,
  DrawerClose,
  DrawerHeader,
  DrawerHeaderActions,
  DrawerHeaderContent,
  DrawerHeaderStart,
  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";

/** `sm` inline column on the start edge — absolute in a relative parent. */
export default function StartSideDemo() {
  return (
    <div className="relative h-[28rem] w-full overflow-hidden rounded-xl border border-subtle bg-layer-2">
      <Drawer swipeDirection="left">
        <div className="flex h-full items-center justify-center p-4">
          <DrawerTrigger
            render={<Button stretch="auto" variant="secondary" size="lg" label="Open navigation" />}
          />
        </div>
        <DrawerPanel variant="inline" side="start" size="sm">
          <DrawerHeader>
            <DrawerHeaderStart>
              <DrawerHeaderContent>
                <DrawerTitle>Navigation</DrawerTitle>
              </DrawerHeaderContent>
            </DrawerHeaderStart>
            <DrawerHeaderActions side="end">
              <DrawerClose
                render={
                  <IconButton
                    variant="ghost"
                    size="md"
                    aria-label="Close"
                    icon={<Icon icon={X} />}
                  />
                }
              />
            </DrawerHeaderActions>
          </DrawerHeader>
          <DrawerBody>Navigation links go here.</DrawerBody>
        </DrawerPanel>
      </Drawer>
    </div>
  );
}

Size

sm 358px (inline absolute column), md 800px / lg 1024px (overlay cards). Header sockets: title cluster on sm; icon toolbars on md/lg (aria-label the panel when there is no DrawerTitle).

Show code
import { Button } from "@makeplane/propel/components/button";
import {
  Drawer,
  DrawerBody,
  DrawerClose,
  DrawerDescription,
  DrawerFooter,
  DrawerHeader,
  DrawerHeaderActions,
  DrawerHeaderContent,
  DrawerHeaderIcon,
  DrawerHeaderStart,
  DrawerPanel,
  DrawerTitle,
  DrawerTrigger,
} from "@makeplane/propel/components/drawer";
import { Icon } from "@makeplane/propel/components/icon";
import { IconButton } from "@makeplane/propel/components/icon-button";
import { ArrowLeft, Box, Maximize2, MoreHorizontal, X } from "lucide-react";

/** `sm` = inline absolute column; `md` / `lg` = overlay (backdrop off). */
export default function SizesDemo() {
  return (
    <div className="flex w-full flex-col gap-6">
      <div className="relative h-[28rem] w-full overflow-hidden rounded-xl border border-subtle bg-layer-2">
        <Drawer swipeDirection="right">
          <div className="flex h-full items-center justify-center p-4">
            <DrawerTrigger
              render={<Button stretch="auto" variant="secondary" size="lg" label="Open sm" />}
            />
          </div>
          <DrawerPanel variant="inline" side="end" size="sm">
            <DrawerHeader>
              <DrawerHeaderStart>
                <DrawerHeaderIcon>
                  <Box />
                </DrawerHeaderIcon>
                <DrawerHeaderContent>
                  <DrawerTitle>Size sm</DrawerTitle>
                  <DrawerDescription>Inline column, 358px.</DrawerDescription>
                </DrawerHeaderContent>
              </DrawerHeaderStart>
              <DrawerHeaderActions side="end">
                <DrawerClose
                  render={
                    <IconButton
                      variant="ghost"
                      size="md"
                      aria-label="Close"
                      icon={<Icon icon={X} />}
                    />
                  }
                />
              </DrawerHeaderActions>
            </DrawerHeader>
            <DrawerBody>Panel body content goes here.</DrawerBody>
            <DrawerFooter>
              <DrawerClose
                render={<Button stretch="auto" variant="ghost" size="md" label="Cancel" />}
              />
              <Button stretch="auto" variant="primary" size="md" label="Save" />
            </DrawerFooter>
          </DrawerPanel>
        </Drawer>
      </div>
      <div className="flex flex-wrap gap-2">
        {(["md", "lg"] as const).map((size) => (
          <Drawer key={size} swipeDirection="right">
            <DrawerTrigger
              render={
                <Button stretch="auto" variant="secondary" size="lg" label={`Open ${size}`} />
              }
            />
            <DrawerPanel variant="overlay" side="end" size={size} aria-label={`Size ${size}`}>
              <DrawerHeader>
                <DrawerHeaderActions side="start">
                  <IconButton
                    variant="ghost"
                    size="md"
                    aria-label="Back"
                    icon={<Icon icon={ArrowLeft} />}
                  />
                  <IconButton
                    variant="ghost"
                    size="md"
                    aria-label="Expand"
                    icon={<Icon icon={Maximize2} />}
                  />
                  <IconButton
                    variant="ghost"
                    size="md"
                    aria-label="More"
                    icon={<Icon icon={MoreHorizontal} />}
                  />
                </DrawerHeaderActions>
                <DrawerHeaderActions side="end">
                  <IconButton
                    variant="ghost"
                    size="md"
                    aria-label="More actions"
                    icon={<Icon icon={MoreHorizontal} />}
                  />
                  <DrawerClose
                    render={
                      <IconButton
                        variant="ghost"
                        size="md"
                        aria-label="Close"
                        icon={<Icon icon={X} />}
                      />
                    }
                  />
                </DrawerHeaderActions>
              </DrawerHeader>
              <DrawerBody>Panel body content goes here.</DrawerBody>
              <DrawerFooter>
                <DrawerClose
                  render={<Button stretch="auto" variant="ghost" size="md" label="Cancel" />}
                />
                <Button stretch="auto" variant="secondary" size="md" label="Secondary" />
                <Button stretch="auto" variant="primary" size="md" label="Save" />
              </DrawerFooter>
            </DrawerPanel>
          </Drawer>
        ))}
      </div>
    </div>
  );
}

Backdrop

backdrop is opt-in (default off) and overlay-only — not available on inline or contained. Pair <Drawer modal> with backdrop when the page should block behind an overlay panel.

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

export default function BackdropDemo() {
  return (
    <Drawer modal swipeDirection="right">
      <DrawerTrigger
        render={<Button stretch="auto" variant="secondary" size="lg" label="Open with backdrop" />}
      />
      <DrawerPanel variant="overlay" backdrop side="end" size="md" aria-label="With backdrop">
        <DrawerHeader>
          <DrawerHeaderActions side="start">
            <IconButton
              variant="ghost"
              size="md"
              aria-label="Back"
              icon={<Icon icon={ArrowLeft} />}
            />
          </DrawerHeaderActions>
          <DrawerHeaderActions side="end">
            <DrawerClose
              render={
                <IconButton variant="ghost" size="md" aria-label="Close" icon={<Icon icon={X} />} />
              }
            />
          </DrawerHeaderActions>
        </DrawerHeader>
        <DrawerBody>Backdrop dims the page behind the panel.</DrawerBody>
        <DrawerFooter>
          <DrawerClose
            render={<Button stretch="auto" variant="ghost" size="md" label="Cancel" />}
          />
          <Button stretch="auto" variant="primary" size="md" label="Save" />
        </DrawerFooter>
      </DrawerPanel>
    </Drawer>
  );
}

Nested

Stacked overlay drawers (no backdrop).

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

export default function NestedDemo() {
  return (
    <Drawer swipeDirection="right">
      <DrawerTrigger
        render={<Button stretch="auto" variant="secondary" size="lg" label="Open settings" />}
      />
      <DrawerPanel variant="overlay" side="end" size="md">
        <DrawerHeader>
          <DrawerHeaderStart>
            <DrawerHeaderContent>
              <DrawerTitle>Settings</DrawerTitle>
              <DrawerDescription>Workspace-wide preferences.</DrawerDescription>
            </DrawerHeaderContent>
          </DrawerHeaderStart>
        </DrawerHeader>
        <DrawerBody>
          <Drawer swipeDirection="right">
            <DrawerTrigger
              render={
                <Button stretch="auto" variant="secondary" size="md" label="Advanced options" />
              }
            />
            <DrawerPanel variant="overlay" side="end" size="md">
              <DrawerHeader>
                <DrawerHeaderStart>
                  <DrawerHeaderContent>
                    <DrawerTitle>Advanced options</DrawerTitle>
                  </DrawerHeaderContent>
                </DrawerHeaderStart>
              </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,
  DrawerHeaderStart,
  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 stretch="auto" variant="secondary" size="lg" label="WEB-101" />}
      />
      <DrawerTrigger
        handle={detailsDrawer}
        payload="WEB-202"
        render={<Button stretch="auto" variant="secondary" size="lg" label="WEB-202" />}
      />
      <Drawer handle={detailsDrawer} swipeDirection="right">
        {({ payload }) => (
          <DrawerPanel variant="overlay" side="end" size="md">
            <DrawerHeader>
              <DrawerHeaderStart>
                <DrawerHeaderContent>
                  <DrawerTitle>{typeof payload === "string" ? payload : "Details"}</DrawerTitle>
                  <DrawerDescription>Work item details.</DrawerDescription>
                </DrawerHeaderContent>
              </DrawerHeaderStart>
            </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,
  AlertDialogBody,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogHeader,
  AlertDialogIntro,
  AlertDialogTitle,
} from "@makeplane/propel/components/alert-dialog";
import { Button } from "@makeplane/propel/components/button";
import {
  Drawer,
  DrawerBody,
  DrawerClose,
  DrawerDescription,
  DrawerHeader,
  DrawerHeaderActions,
  DrawerHeaderContent,
  DrawerHeaderStart,
  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
        swipeDirection="right"
        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 stretch="auto" variant="secondary" size="lg" label="Add comment" />}
        />
        <DrawerPanel variant="overlay" side="end" size="md">
          <DrawerHeader>
            <DrawerHeaderStart>
              <DrawerHeaderContent>
                <DrawerTitle>New comment</DrawerTitle>
                <DrawerDescription>Share feedback on this work item.</DrawerDescription>
              </DrawerHeaderContent>
            </DrawerHeaderStart>
            <DrawerHeaderActions side="end">
              <DrawerClose
                render={
                  <IconButton
                    variant="ghost"
                    size="md"
                    aria-label="Close"
                    icon={<Icon icon={X} />}
                  />
                }
              />
            </DrawerHeaderActions>
          </DrawerHeader>
          <DrawerBody>
            <TextArea
              size="xl"
              surface="field"
              rows={4}
              aria-label="Comment"
              value={draft}
              onChange={(event) => setDraft(event.target.value)}
            />
          </DrawerBody>
        </DrawerPanel>
      </Drawer>
      <AlertDialog open={confirmOpen} onOpenChange={setConfirmOpen}>
        <AlertDialogContent>
          <AlertDialogBody>
            <AlertDialogHeader>
              <AlertDialogIntro>
                <AlertDialogTitle>Discard comment?</AlertDialogTitle>
                <AlertDialogDescription>
                  Your draft will be lost if you close the panel now.
                </AlertDialogDescription>
              </AlertDialogIntro>
            </AlertDialogHeader>
          </AlertDialogBody>
          <AlertDialogActions>
            <Button
              stretch="auto"
              variant="ghost"
              size="md"
              onClick={() => setConfirmOpen(false)}
              label="Keep editing"
            />
            <Button
              stretch="auto"
              variant="danger"
              size="md"
              onClick={() => {
                setConfirmOpen(false);
                setOpen(false);
                setDraft("");
              }}
              label="Discard"
            />
          </AlertDialogActions>
        </AlertDialogContent>
      </AlertDialog>
    </>
  );
}

Page shift

Wrap the page in a DrawerProvider with a DrawerIndentBackground before a DrawerIndent around the content and the Drawer. While any drawer in the provider is open, both indent parts carry data-active, so the page can scale inward and reveal the background behind it.

Open the drawer to indent this page.

Show code
import { Button } from "@makeplane/propel/components/button";
import {
  Drawer,
  DrawerBody,
  DrawerClose,
  DrawerDescription,
  DrawerFooter,
  DrawerHeader,
  DrawerHeaderActions,
  DrawerHeaderContent,
  DrawerHeaderStart,
  DrawerIndent,
  DrawerIndentBackground,
  DrawerPanel,
  DrawerProvider,
  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 PageShiftDemo() {
  return (
    <DrawerProvider>
      <div className="relative overflow-hidden rounded-xl border border-subtle-1">
        <DrawerIndentBackground render={<div className="absolute inset-0 bg-layer-2" />} />
        <DrawerIndent
          render={
            <div className="relative bg-layer-1 p-4 transition-transform duration-300 data-active:scale-[0.98] data-active:rounded-xl" />
          }
        >
          <div className="flex flex-wrap items-center gap-3">
            <Drawer swipeDirection="right">
              <DrawerTrigger
                render={
                  <Button stretch="auto" variant="secondary" size="lg" label="Open details" />
                }
              />
              <DrawerPanel variant="overlay" side="end" size="md">
                <DrawerHeader>
                  <DrawerHeaderStart>
                    <DrawerHeaderContent>
                      <DrawerTitle>Work item details</DrawerTitle>
                      <DrawerDescription>
                        The page indents while this drawer is open.
                      </DrawerDescription>
                    </DrawerHeaderContent>
                  </DrawerHeaderStart>
                  <DrawerHeaderActions side="end">
                    <DrawerClose
                      render={
                        <IconButton
                          variant="ghost"
                          size="md"
                          aria-label="Close"
                          icon={<Icon icon={X} />}
                        />
                      }
                    />
                  </DrawerHeaderActions>
                </DrawerHeader>
                <DrawerBody>Panel body content goes here.</DrawerBody>
                <DrawerFooter>
                  <DrawerClose
                    render={<Button stretch="auto" variant="ghost" size="md" label="Cancel" />}
                  />
                  <Button stretch="auto" variant="secondary" size="md" label="Secondary" />
                  <Button stretch="auto" variant="primary" size="md" label="Save" />
                </DrawerFooter>
              </DrawerPanel>
            </Drawer>
            <p className="text-body-sm-regular text-secondary">
              Open the drawer to indent this page.
            </p>
          </div>
        </DrawerIndent>
      </div>
    </DrawerProvider>
  );
}

API Reference

Drawer

The drawer Root that holds open state and gesture configuration. Uncontrolled by default; pass `defaultOpen` (uncontrolled) or `open` + `onOpenChange` (controlled). A behavior-only role that renders no element of its own (rules 1a, 2), so it lives in `components`; the styled parts live in `elements/drawer` and are grafted onto Base UI behavior here.

PropTypeDefaultDescription
childrenReactNode | PayloadChildRenderFunction<unknown>The content of the drawer.
modalboolean | "trap-focus"falseModal while open. Off by default so inline/layout drawers keep the page interactive. Pass `true` or `"trap-focus"` for blocking overlays (typically with `DrawerPanel backdrop`).
swipeDirection"up" | "down" | "left" | "right"Swipe-to-dismiss direction. Pair with `DrawerPanel` `side`: `"end"` → `"right"`, `"start"` → `"left"`. Base UI defaults this to `"down"` (vertical), which is wrong for horizontal edge drawers — set it explicitly.

DrawerTrigger

The behavior that opens the drawer. A behavior-only role that carries no propel styling, so graft it onto a styled control via `render` — behavior part outer, styled part as the render target, so the control's look wins: ```tsx <DrawerTrigger render={<Button stretch="auto" variant="secondary" size="lg" label="Open details" />} />; ``` Base UI manages `aria-haspopup`/`aria-expanded` and focus restoration when the drawer closes. Maps 1:1 to `Drawer.Trigger`.

PropTypeDefaultDescription
renderReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, DrawerTriggerState>Allows 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.

DrawerPanel

Convenience that composes drawer portal/viewport/popup boilerplate. Defaults to `inline` + `sm` (flush absolute column in a relative parent). `overlay` portals to `document.body`; `contained` requires a parent `container`. `backdrop` is overlay-only.

PropTypeDefaultDescription
side(required)"end" | "start"The 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 chrome both follow this value. Pair with `Drawer` `swipeDirection`: `"end"` → `"right"`, `"start"` → `"left"`.
size"sm" | "md" | "lg""sm"Panel width: `sm` 358px, `md` 800px, `lg` 1024px. Prefer `sm` with `inline`; `md` / `lg` with `overlay` / `contained`.
childrenReactNodeThe panel body — typically a `DrawerHeader`, `DrawerBody`, and `DrawerFooter`.
variant"inline" | "overlay" | "contained""inline"Placement + chrome. Defaults to the flush absolute column (`inline`). Floating card portaled to `document.body` (viewport-fixed). Prefer `md` / `lg`. Floating card portaled into a positioned parent shell (`container` required).
containerDrawerPortalContainer | nullPortal target. When omitted, uses an absolute-fill host (parent must be `relative`). Pass a positioned shell when you manage the host yourself. Portal target. When omitted, uses `document.body`. Positioned parent the floating card portals into (required for `contained`).
backdropbooleanfalseWhen true, mounts a dimmed viewport-fixed `DrawerBackdrop`. Only available on `overlay` (pair with `<Drawer modal>`).

DrawerBackdrop

The dimmed overlay behind the drawer: Base UI's `Drawer.Backdrop` behavior grafted onto the shared `internal/Backdrop` surface (rule 4a). Fades in/out via Base UI's `data-starting-style`/`data-ending-style` transition hooks.

PropTypeDefaultDescription
renderReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, DrawerBackdropState>Allows 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.

DrawerTitle

The accessible heading for the drawer: Base UI's `Drawer.Title` behavior (wired as the popup's `aria-labelledby` target) grafted onto the shared `internal/OverlayTitle` at `size="xl"` (body-md semibold per Figma; rule 4a).

PropTypeDefaultDescription
renderReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, DrawerTitleState>Allows 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.

DrawerDescription

Supporting text for the drawer: Base UI's `Drawer.Description` behavior (wired as the popup's `aria-describedby` target) grafted onto the shared `internal/OverlayDescription` at `size="md"` (body-xs per Figma; rule 4a).

PropTypeDefaultDescription
renderReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, DrawerDescriptionState>Allows 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.

DrawerHeader

The drawer's top region (the Figma header). Compose a start cluster (`DrawerHeaderStart` or `DrawerHeaderActions side="start"`) and an end cluster (`DrawerHeaderActions side="end"`) for the title/icon sockets.

PropTypeDefaultDescription
renderReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, {}>Allows 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.

DrawerHeaderStart

The header's inline-start title cluster (Figma narrow header). Holds an optional `DrawerHeaderIcon` / `IconButton` plus `DrawerHeaderContent` (title + description).

PropTypeDefaultDescription
renderReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, {}>Allows 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.

DrawerHeaderIcon

Decorative leading glyph beside the title inside `DrawerHeaderStart`. Sizes a bare svg/img child; interactive leading controls should be an `IconButton` instead. Bakes `aria-hidden`.

PropTypeDefaultDescription
renderReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, {}>Allows 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.

DrawerHeaderContent

The stacked title + description inside a `DrawerHeader`. Groups them at the header's inline-start so a corner close can sit opposite at the inline-end; a long title wraps instead of pushing the close off the row.

PropTypeDefaultDescription
renderReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, {}>Allows 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.

DrawerHeaderActions

Header icon-button toolbar. `side="start"` / `side="end"` pin the cluster to the header's inline edges (Figma top-left / top-right sockets). Pass `IconButton`s and/or `DrawerClose`.

PropTypeDefaultDescription
side(required)"start" | "end"
renderReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, {}>Allows 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.

DrawerBody

The drawer's main content region (the Figma body). Grows to fill the space between the header and footer and scrolls its own overflow.

PropTypeDefaultDescription
renderReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, {}>Allows 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.

DrawerFooter

The drawer's footer actions region (the Figma footer). Lays its actions at the inline-end.

PropTypeDefaultDescription
renderReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, {}>Allows 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.

DrawerClose

The behavior that closes the drawer when activated. A behavior-only role that carries no propel styling, so graft it onto a styled control via `render` — behavior part outer, styled part as the render target, so the control's look wins: ```tsx <DrawerClose render={ <IconButton variant="ghost" size="md" aria-label={closeLabel} icon={<Icon icon={X} />} /> } />; ``` Maps 1:1 to `Drawer.Close`. (The styled bare close control it replaces in this entry still lives at `elements/drawer` for assembling a drawer from the atoms.)

PropTypeDefaultDescription
renderReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, DrawerCloseState>Allows 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.

DrawerProvider

Optional context wrapper that coordinates indent effects across sibling drawers. Wrap `Drawer` (and any `DrawerIndent`/`DrawerIndentBackground`) in it when you need the page to shift as the drawer opens. A behavior-only provider role (rules 1a, 2), so it lives in `components`. Maps 1:1 to Base UI's `Drawer.Provider`.

PropTypeDefaultDescription
childrenReactNodeThe `Drawer`(s) and any `DrawerIndent`/`DrawerIndentBackground` to coordinate.

DrawerIndent

Wraps page content that should scale/shift inward as the drawer opens (the indent effect). Place inside a `DrawerProvider`, around the `Drawer` root and your page. A behavior-only role (rules 1a, 2), so it lives in `components`. Maps 1:1 to Base UI's `Drawer.Indent`.

PropTypeDefaultDescription
renderReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, DrawerIndentState>Allows 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.

DrawerIndentBackground

Renders behind the indented page content so the area revealed by the indent effect has a backdrop color. Used alongside `DrawerIndent` inside a `DrawerProvider`. A behavior-only role (rules 1a, 2), so it lives in `components`. Maps 1:1 to Base UI's `Drawer.IndentBackground`.

PropTypeDefaultDescription
renderReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, DrawerIndentBackgroundState>Allows 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.

DrawerSwipeArea

An off-screen edge hit area that lets users swipe the closed drawer open from the screen edge. Optional; render it as a sibling of the trigger. A behavior-only role (rules 1a, 2), so it lives in `components`. Maps 1:1 to Base UI's `Drawer.SwipeArea`.

PropTypeDefaultDescription
renderReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, DrawerSwipeAreaState>Allows 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.