Skip to main content

Drawer

Drawer is a panel that slides in from an edge of the screen, like a filters panel or a mobile action sheet. It can snap to set heights, be dragged to close, and stack nested drawers.

Import

import { Drawer } from '@vortexlabs/vortex';
import type { DrawerProps } from '@vortexlabs/vortex';

Other links

"use client";

import { Button, Drawer, Stack, Text } from "@vortexlabs/vortex";
import { useId } from "react";

export const ProjectDrawer = () => {
  const titleId = useId();

  return (
    <Drawer.Root>
      <Drawer.Trigger>
        {(attributes) => <Button attributes={attributes}>Open drawer</Button>}
      </Drawer.Trigger>
      <Drawer.Portal>
        <Drawer.Content attributes={{ "aria-labelledby": titleId }}>
          <Stack padding={6} gap={4} align="start">
            <Text as="h2" size="heading-sm-desktop" attributes={{ id: titleId }}>
              Project details
            </Text>
            <Text>Review the project before continuing.</Text>
            <Drawer.Close>
              {(attributes) => (
                <Button variant="secondary" attributes={attributes}>
                  Close
                </Button>
              )}
            </Drawer.Close>
          </Stack>
        </Drawer.Content>
      </Drawer.Portal>
    </Drawer.Root>
  );
};

Features

  • Opens from the top, bottom, start, or end.
  • Snaps to set heights with a draggable grabber.
  • Blocks the page or keeps it interactive.
  • Supports nested drawers.

Structure

Root holds the state, Trigger and Close pass open and close handlers to your own buttons, Portal renders the backdrop, and Content is the panel.

<Drawer.Root>
  <Drawer.Trigger>{(attributes) => <Button attributes={attributes}>Open</Button>}</Drawer.Trigger>
  <Drawer.Portal>
    <Drawer.Content attributes={{ "aria-label": "Filters" }}>
      <Drawer.Close>{(attributes) => <Button attributes={attributes}>Close</Button>}</Drawer.Close>
    </Drawer.Content>
  </Drawer.Portal>
</Drawer.Root>

Open State

Pass open and onOpenChange to control the drawer, or defaultOpen to let it manage itself. dismissible={false} stops Escape, backdrop clicks, and dragging from closing it.

const [open, setOpen] = useState(false);

<Drawer.Root open={open} onOpenChange={setOpen} dismissible={false}>
  <Drawer.Trigger>
    {(attributes) => (
      <Button variant="secondary" attributes={attributes}>
        Review publishing
      </Button>
    )}
  </Drawer.Trigger>
  <Drawer.Portal>
    <Drawer.Content attributes={{ "aria-label": "Publishing details" }}>
      <Stack padding={6} gap={4} align="start">
        <Text as="h2" size="heading-sm-desktop">Publishing details</Text>
        <Text>This panel stays open until you choose an action.</Text>
        <Stack direction="row" gap={3}>
          <Button variant="secondary" onClick={() => setOpen(false)}>Cancel</Button>
          <Button onClick={() => setOpen(false)}>Publish</Button>
        </Stack>
      </Stack>
    </Drawer.Content>
  </Drawer.Portal>
</Drawer.Root>

Variants

variant sets the backdrop style: dim (default), blurred, or transparent.

<Drawer.Root variant="dim">…</Drawer.Root>
<Drawer.Root variant="blurred">…</Drawer.Root>
<Drawer.Root variant="transparent">…</Drawer.Root>

Position

Drawers open from the bottom by default. Set position to top, start, or end to change the edge. start and end follow the reading direction and are the only positions that accept a width, which defaults to 384px.

<Drawer.Root position="top">…</Drawer.Root>
<Drawer.Root position="bottom">…</Drawer.Root>
<Drawer.Root position="start" width={{ xs: "90vw", md: "24rem" }}>…</Drawer.Root>
<Drawer.Root position="end" width={{ xs: "90vw", md: "24rem" }}>…</Drawer.Root>

Snap Points

Top and bottom drawers can rest at snapPoints: numbers are a share of the screen height, "240px" strings are fixed heights. grabber adds a handle to change the size.

<Drawer.Root snapPoints={[0.4, 0.7, 0.95]} grabber>…</Drawer.Root>

Custom Handle

Place Drawer.Handle first or last in Content. With dragHandleOnly, only the handle starts a drag, so the content stays selectable.

<Drawer.Root snapPoints={["240px", "480px"]} dragHandleOnly>
  …
  <Drawer.Portal>
    <Drawer.Content attributes={{ "aria-label": "Project notes" }}>
      <Drawer.Handle attributes={{ "aria-label": "Resize project notes" }} />
      …
    </Drawer.Content>
  </Drawer.Portal>
</Drawer.Root>

Background Interaction

backgroundInteraction="interactive" keeps the page usable while the drawer is open. A transparent backdrop alone still blocks the page.

<Drawer.Root
  position="end"
  width="20rem"
  backgroundInteraction="interactive"
  variant="transparent"
>
  …
</Drawer.Root>

Nested Drawers

Use Drawer.NestedRoot inside a drawer's Content to open another drawer on top. It takes the same props as Root.

<Drawer.Content attributes={{ "aria-label": "Project overview" }}>
  <Drawer.NestedRoot>
    <Drawer.Trigger>
      {(attributes) => <Button attributes={attributes}>View milestones</Button>}
    </Drawer.Trigger>
    <Drawer.Portal>
      <Drawer.Content attributes={{ "aria-label": "Milestones" }}>…</Drawer.Content>
    </Drawer.Portal>
  </Drawer.NestedRoot>
</Drawer.Content>

Accessibility

Key
Description
Escape
Closes the topmost drawer unless dismissible is false.
Up arrowDown arrow
On the handle, moves to the next or previous snap point.
HomeEnd
On the handle, moves to the smallest or largest snap point.
EnterSpace
On the handle, cycles through snap points, or closes the drawer if there are fewer than two.
Name Content with aria-labelledby pointing at its title, or aria-label if it has none.
Add grabber or Drawer.Handle to a drawer with snapPoints so keyboard users can resize it.
Focus stays in the drawer until it closes, unless backgroundInteraction is "interactive".