Skip to main content

Drawer

Drawer is an overlay that slides a panel in from an edge of the screen, such as a filters panel or a mobile action sheet.

Import

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

Allied components

Other links

"use client";

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

export const ProjectDrawer = () => {
  const titleId = React.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.

Anatomy

<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>
Component Part
Description
Holds the open state, position, and snap points.
Gives your button the attributes that open the drawer.
Renders the backdrop and panel into document.body or a container.
Shows the panel that slides in from the edge.
Gives your button the attributes that close the drawer.
Drags the drawer and steps through snap points.
Opens another drawer on top of this one.

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
EnterSpace
On the handle, cycles through snap points, or closes the drawer if there are fewer than two.
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.
Escape
Closes the topmost drawer unless dismissible is false.
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".