Drawer
A drawer is a Modal that comes in from an edge and can be dragged back out. It suits a panel the reader opens and closes repeatedly — filters, a cart, a detail view — where a dialog landing in the middle of the page would be heavier than the task deserves.
import {
Drawer,
DrawerBackdrop,
DrawerBody,
DrawerContent,
DrawerDialog,
DrawerHandle,
DrawerHeading,
} from "ropav";The layers match the modal's: DrawerBackdrop dims the page, DrawerContent decides which edge the drawer comes from, DrawerDialog is the panel. DrawerHandle is the grab bar, and DrawerHeader, DrawerBody and DrawerFooter divide the panel so only the body scrolls.
drawer-placement.vue
<script setup lang="ts">
import {
Button,
Drawer,
DrawerBackdrop,
DrawerBody,
DrawerClose,
DrawerContent,
DrawerDialog,
DrawerFooter,
DrawerHandle,
DrawerHeader,
DrawerHeading,
} from "ropav";
const placements = ["bottom", "right"] as const;
</script>
<template>
<Drawer v-for="placement in placements" :key="placement">
<Button class="side" variant="secondary">{{ placement }}</Button>
<DrawerBackdrop>
<DrawerContent :placement="placement">
<DrawerDialog>
<DrawerHandle />
<DrawerHeader>
<DrawerHeading>Filters</DrawerHeading>
</DrawerHeader>
<DrawerBody>
<p class="note">
The handle is draggable, so the drawer can be dismissed by flicking it away.
</p>
</DrawerBody>
<DrawerFooter>
<DrawerClose><Button variant="tertiary">Reset</Button></DrawerClose>
<DrawerClose><Button>Apply</Button></DrawerClose>
</DrawerFooter>
</DrawerDialog>
</DrawerContent>
</DrawerBackdrop>
</Drawer>
</template>
<style scoped>
.side {
text-transform: capitalize;
}
.note {
color: var(--rp-muted);
font-size: var(--rp-text-sm);
line-height: var(--rp-text-sm--line-height);
}
</style>Which edge
placement on DrawerContent takes top, bottom, left or right. Bottom is the phone default and reads as a sheet; the sides read as a panel and suit a filter list or a detail view beside the content it belongs to.
Dismissal and the handle
is-dismissable on DrawerBackdrop controls both the click outside and whether the drawer can be dragged away — one setting for both, because a drawer you can drag but not dismiss would spring back every time.
The drag lives on DrawerDialog, not on the handle: the whole panel answers the gesture, and DrawerHandle is only the bar that says so.
portal-container says where the drawer is rendered, for an app that is not mounted on body. A PortalProvider says it once for every overlay below it.
Accessibility
- Focus moves into the drawer and back to the trigger, and the page behind is inert while it is open — the same guarantees a modal gives, because it is the same machinery.
DrawerHeadingnames the dialog. A drawer without one is announced as an unnamed dialog, which is worse here than in a modal because a drawer is often opened without a deliberate click.DrawerHandleisaria-hiddenand has no content slot — it is a pointer affordance with nothing to announce. Dragging is never the only way out: Esc and the backdrop both close the drawer, so a reader who cannot drag is not stuck.DrawerDialoghandscloseto its slot, for a control that has to finish something before the drawer goes.
API
Drawer
| Prop | Type | Default | Description |
|---|---|---|---|
defaultOpen | boolean | — | |
isDisabled | boolean | — | |
isOpen | boolean | — | |
state | OverlayTriggerState | — | An open state held outside the drawer, for a caller driving several overlays from one place. Equivalent to passing isOpen and listening to openChange, and takes precedence over both. |
Slots: default.
| Event | Payload |
|---|---|
openChange | boolean |
update:isOpen | boolean |
DrawerBackdrop
| Prop | Type | Default | Description |
|---|---|---|---|
isDismissable | boolean | — | Whether an interaction outside the drawer closes it, and whether it can be dragged away. |
isEntering | boolean | — | Forces the entry state on both elements, for a caller driving the animation itself. |
isExiting | boolean | — | Forces the exit state on both elements. |
isKeyboardDismissDisabled | boolean | false | |
portalContainer | string | HTMLElement | document.body | Where the drawer is rendered. |
shouldCloseOnInteractOutside | ((element: Element) => boolean) | — | Filters which outside elements dismiss the drawer. |
style | string | CSSProperties | — | Inline style for the backdrop. Declared rather than left to fallthrough because the viewport custom properties are merged over it: the stylesheet sizes the drawer from them, and a caller's own style winning would leave it with no height at all. |
variant | "blur" | "opaque" | "transparent" | "opaque" | How the page behind the drawer is treated. |
Slots: default.
DrawerBody
Slots: default.
DrawerClose
Slots: default.
DrawerCloseTrigger
DrawerCloseTrigger takes nothing but class.
DrawerContent
| Prop | Type | Default | Description |
|---|---|---|---|
placement | "bottom" | "left" | "right" | "top" | "bottom" |
Slots: default.
DrawerDialog
The default slot receives:
| Slot prop | Type |
|---|---|
close | () => void |
DrawerFooter
Slots: default.
DrawerHandle
DrawerHandle takes nothing but class.
DrawerHeader
Slots: default.
DrawerHeading
| Prop | Type | Default | Description |
|---|---|---|---|
level | number | 2 | Heading level. |
Slots: default.
DrawerTrigger
| Prop | Type | Default | Description |
|---|---|---|---|
isDisabled | boolean | — |
Slots: default.