"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
dismissible is false.Content with aria-labelledby pointing at its title, or aria-label if it has none.grabber or Drawer.Handle to a drawer with snapPoints so keyboard users can resize it.backgroundInteraction is "interactive".