Skip to content

Pickers and overlays

Drawer

An edge-anchored modal panel that slides in and can be swiped away.

When to use it: Use it for secondary tasks such as settings, filters, or a cart that belong beside the page rather than over its center.

On this page

Example

Loading example…
Drawer.tsxtsx
import { Button, Drawer, DrawerContent, DrawerTrigger } from "@comp0/react";
import { XMarkIcon } from "@heroicons/react/20/solid";

export function Example() {
  return (
    <Drawer side="right">
      <DrawerTrigger className="rounded bg-teal-700 px-3 py-2.5 text-base text-white sm:py-2 sm:text-sm dark:bg-teal-400 dark:text-zinc-950">
        Open settings
      </DrawerTrigger>
      <DrawerContent
        aria-labelledby="drawer-settings-title"
        className="fixed m-0 h-dvh max-h-none w-[min(22rem,calc(100vw-2rem))] border-0 bg-white p-5 text-zinc-900 opacity-100 shadow-2xl ring-1 ring-zinc-950/10 transition-[opacity,translate] duration-150 ease-out data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=right]:left-auto data-[side=right]:translate-x-0 data-[side=right]:starting:translate-x-4 starting:opacity-0 data-dragging:transition-none motion-reduce:transition-none backdrop:bg-zinc-950/40 dark:bg-zinc-900 dark:text-zinc-50 dark:shadow-none dark:ring-white/10"
      >
        <div className="flex h-full flex-col gap-5">
          <div className="flex items-start justify-between gap-4">
            <div className="grid gap-1">
              <h2 id="drawer-settings-title" className="text-lg font-semibold">
                Settings
              </h2>
              <p className="text-base text-zinc-600 sm:text-sm dark:text-zinc-400">
                Drag the panel toward the right edge to dismiss it, or press Escape.
              </p>
            </div>
            <form method="dialog">
              <Button
                type="submit"
                aria-label="Close settings"
                className="rounded p-2 text-zinc-500 outline-teal-600 hover:bg-zinc-100 focus-visible:outline-2 dark:text-zinc-400 dark:outline-teal-400 dark:hover:bg-zinc-800"
              >
                <XMarkIcon className="size-5" aria-hidden="true" />
              </Button>
            </form>
          </div>
          <div className="grid gap-3 text-base sm:text-sm">
            <label className="flex items-center justify-between gap-4">
              Product updates
              <input type="checkbox" defaultChecked className="size-4 accent-teal-700" />
            </label>
            <label className="flex items-center justify-between gap-4">
              Weekly digest
              <input type="checkbox" className="size-4 accent-teal-700" />
            </label>
          </div>
        </div>
      </DrawerContent>
    </Drawer>
  );
}

Anatomy

Dashed frames are invisible state providers; shaded shapes own real DOM. Numbered pins match the list below.

A wireframe sketch of the assembled component. Each numbered marker matches a part in the list that follows.

  1. Drawer

    Wrapper-free provider for the drawer's open state and anchored side. Does not add a DOM element.

  2. DrawerTrigger

    Button that opens the drawer. Owns a DOM element.

  3. DrawerContent

    Native modal panel anchored to one edge; dragging it toward that edge dismisses it. Owns a DOM element.

Step by step

  1. 1

    Add the main part

    Start Drawer with DrawerTrigger and pick the side the panel anchors to.

  2. 2

    Add the supporting parts

    Put a labelled DrawerContent after the trigger and anchor its styles with the data-side attribute.

  3. 3

    Make the behavior clear

    Keep an explicit close action inside; dragging the panel toward its edge or pressing Escape also dismisses it.

    Exampletsx
    <Drawer side="right">
      <DrawerTrigger>Open settings</DrawerTrigger>
      <DrawerContent aria-labelledby="settings-title">
        <h2 id="settings-title">Settings</h2>
      </DrawerContent>
    </Drawer>;

Keyboard

Esc
Closes and restores trigger focus.
Cycles inside the modal panel.

Forms and accessibility

Forms inside DrawerContent submit normally; method=dialog closes the drawer without navigation.

Accessibility checklist

  • Connect DrawerContent to a visible heading with aria-labelledby.
  • The swipe gesture is purely additive; keep an explicit close action for keyboard and assistive users.
  • Focus stays inside the modal panel while it is open and returns to the trigger on close.

API reference

Importtsx
import { Drawer, DrawerContent, DrawerTrigger } from "@comp0/react";

Drawer

Context only

Wrapper-free provider for the drawer's open state and anchored side.

PropTypeDescription
openbooleanControlled open state.
defaultOpenbooleanInitial open state.
onToggle(open: boolean) => voidReceives the next open state.
side"left" | "right" | "top" | "bottom"Edge the panel anchors to; defaults to "right".

DrawerTrigger

DOM element

Button that opens the drawer.

PropTypeDescription
asElementType | FragmentFragment merges the trigger onto your own element child.

DrawerContent

DOM element

Native modal panel anchored to one edge; dragging it toward that edge dismisses it.

PropTypeDescription
aria-labelledbystringPoints to the drawer's visible heading.
portalbooleanRenders into document.body; on by default.
closedby"any" | "closerequest" | "none"Native dismissal policy; any adds light dismiss where supported.

Style hooks

Attributes that appear while a state is true. Target them with Tailwind data variants such as data-open:bg-zinc-100, or with any CSS selector.

DrawerTrigger

Style hookMeaning
[data-open]The drawer is open.

DrawerContent

Style hookMeaning
[data-open]The drawer is open.
[data-side]The anchored edge, for positioning and slide transitions.
[data-dragging]A dismiss drag is following the pointer.

Keep exploring