Drawer
Drawer is a slide-out panel for supplementary content and actions.
Import
vue
<script setup lang="ts">
import {
Drawer,
DrawerBackdrop,
DrawerBody,
DrawerCloseTrigger,
DrawerContent,
DrawerDialog,
DrawerFooter,
DrawerHandle,
DrawerHeader,
DrawerHeading,
DrawerTrigger,
} from '@rysinal/heroui-vue'
</script>Usage
Default
<template>
<Drawer>
<Button variant="secondary">Open Drawer</Button>
<DrawerBackdrop>
<DrawerContent placement="right">
<DrawerDialog>
<DrawerHeader>
<DrawerHeading>Drawer Title</DrawerHeading>
</DrawerHeader>
<DrawerBody>
<p>
This is a right drawer built with the Vue Drawer component. It slides in from the
edge of the screen with a smooth CSS transition.
</p>
</DrawerBody>
<DrawerFooter>
<Button data-drawer-close="true" variant="secondary">Cancel</Button>
<Button data-drawer-close="true">Confirm</Button>
</DrawerFooter>
</DrawerDialog>
</DrawerContent>
</DrawerBackdrop>
</Drawer>
</template>
<script setup lang="ts">
import {
Button,
Drawer,
DrawerBackdrop,
DrawerBody,
DrawerContent,
DrawerDialog,
DrawerFooter,
DrawerHeader,
DrawerHeading,
} from '@rysinal/heroui-vue'
</script>Anatomy
vue
<template>
<Drawer>
<Button>Open Drawer</Button>
<DrawerBackdrop>
<DrawerContent>
<DrawerDialog>
<DrawerHandle />
<DrawerCloseTrigger />
<DrawerHeader>
<DrawerHeading>Title</DrawerHeading>
</DrawerHeader>
<DrawerBody>Drawer content</DrawerBody>
<DrawerFooter />
</DrawerDialog>
</DrawerContent>
</DrawerBackdrop>
</Drawer>
</template>Placement
<template>
<div class="flex flex-wrap gap-4">
<Drawer v-for="placement in placements" :key="placement">
<Button variant="secondary">{{ placementLabel(placement) }}</Button>
<DrawerBackdrop>
<DrawerContent :placement="placement">
<DrawerDialog>
<DrawerCloseTrigger />
<DrawerHandle v-if="placement === 'bottom'" />
<DrawerHeader>
<DrawerHeading>{{ placementLabel(placement) }} Drawer</DrawerHeading>
</DrawerHeader>
<DrawerBody>
<p>
This drawer slides in from the <strong>{{ placement }}</strong> edge of the screen.
</p>
</DrawerBody>
<DrawerFooter>
<Button data-drawer-close="true" variant="secondary">Cancel</Button>
<Button data-drawer-close="true">Done</Button>
</DrawerFooter>
<DrawerHandle v-if="placement === 'top'" />
</DrawerDialog>
</DrawerContent>
</DrawerBackdrop>
</Drawer>
</div>
</template>
<script setup lang="ts">
import {
Button,
Drawer,
DrawerBackdrop,
DrawerBody,
DrawerCloseTrigger,
DrawerContent,
DrawerDialog,
DrawerFooter,
DrawerHandle,
DrawerHeader,
DrawerHeading,
type DrawerPlacement,
} from '@rysinal/heroui-vue'
const placements: DrawerPlacement[] = ['bottom', 'top', 'left', 'right']
const placementLabel = (placement: DrawerPlacement) =>
placement.charAt(0).toUpperCase() + placement.slice(1)
</script>Backdrop Variants
<template>
<div class="flex flex-wrap gap-4">
<Drawer v-for="variant in variants" :key="variant">
<Button variant="secondary">{{ variantLabel(variant) }}</Button>
<DrawerBackdrop :variant="variant">
<DrawerContent>
<DrawerDialog>
<DrawerHandle />
<DrawerCloseTrigger />
<DrawerHeader>
<DrawerHeading>Backdrop: {{ variantLabel(variant) }}</DrawerHeading>
</DrawerHeader>
<DrawerBody>
<p>
This drawer uses the <code>{{ variant }}</code> backdrop variant.
</p>
</DrawerBody>
<DrawerFooter>
<Button class="w-full" data-drawer-close="true">Close</Button>
</DrawerFooter>
</DrawerDialog>
</DrawerContent>
</DrawerBackdrop>
</Drawer>
</div>
</template>
<script setup lang="ts">
import {
Button,
Drawer,
DrawerBackdrop,
DrawerBody,
DrawerCloseTrigger,
DrawerContent,
DrawerDialog,
DrawerFooter,
DrawerHandle,
DrawerHeader,
DrawerHeading,
type DrawerBackdropVariant,
} from '@rysinal/heroui-vue'
const variants: DrawerBackdropVariant[] = ['opaque', 'blur', 'transparent']
const variantLabel = (variant: DrawerBackdropVariant) =>
variant.charAt(0).toUpperCase() + variant.slice(1)
</script>Non-Dismissable
<template>
<Drawer>
<Button variant="secondary">Important Action</Button>
<DrawerBackdrop :is-dismissable="false">
<DrawerContent>
<DrawerDialog>
<DrawerHeader>
<DrawerHeading>Confirm Action</DrawerHeading>
</DrawerHeader>
<DrawerBody>
<p>
This drawer cannot be dismissed by clicking outside or dragging. You must use one of
the buttons below.
</p>
</DrawerBody>
<DrawerFooter>
<Button data-drawer-close="true" variant="secondary">Cancel</Button>
<Button data-drawer-close="true">Confirm</Button>
</DrawerFooter>
</DrawerDialog>
</DrawerContent>
</DrawerBackdrop>
</Drawer>
</template>
<script setup lang="ts">
import {
Button,
Drawer,
DrawerBackdrop,
DrawerBody,
DrawerContent,
DrawerDialog,
DrawerFooter,
DrawerHeader,
DrawerHeading,
} from '@rysinal/heroui-vue'
</script>Scrollable Content
<template>
<Drawer>
<Button variant="secondary">Terms & Conditions</Button>
<DrawerBackdrop>
<DrawerContent>
<DrawerDialog>
<DrawerHandle />
<DrawerCloseTrigger />
<DrawerHeader>
<DrawerHeading>Terms & Conditions</DrawerHeading>
</DrawerHeader>
<DrawerBody>
<p v-for="index in 20" :key="index" class="mb-3">
Paragraph {{ index }}: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam
pulvinar risus non risus hendrerit venenatis. Pellentesque sit amet hendrerit risus,
sed porttitor quam.
</p>
</DrawerBody>
<DrawerFooter>
<Button data-drawer-close="true" variant="secondary">Decline</Button>
<Button data-drawer-close="true">Accept</Button>
</DrawerFooter>
</DrawerDialog>
</DrawerContent>
</DrawerBackdrop>
</Drawer>
</template>
<script setup lang="ts">
import {
Button,
Drawer,
DrawerBackdrop,
DrawerBody,
DrawerCloseTrigger,
DrawerContent,
DrawerDialog,
DrawerFooter,
DrawerHandle,
DrawerHeader,
DrawerHeading,
} from '@rysinal/heroui-vue'
</script>Controlled State
With ref()
Control the drawer using Vue's ref for simple state management.
Status: closed
With small state helpers
Use a tiny local helper object for convenient methods like open(), close(), and toggle().
Status: closed
<template>
<div class="flex max-w-md flex-col gap-8">
<div class="flex flex-col gap-3">
<h3 class="m-0 text-lg font-semibold text-foreground">With ref()</h3>
<p class="m-0 text-sm leading-relaxed text-pretty text-muted">
Control the drawer using Vue's <code class="text-foreground">ref</code> for simple state
management.
</p>
<div class="flex flex-col items-start gap-3 rounded-2xl bg-surface p-4 shadow-sm">
<div class="flex w-full items-center justify-between">
<p class="m-0 text-xs text-muted">
Status:
<span class="font-mono font-medium text-foreground">
{{ isOpen ? 'open' : 'closed' }}
</span>
</p>
</div>
<div class="flex gap-2">
<Button size="sm" variant="secondary" @click="isOpen = true">Open Drawer</Button>
<Button size="sm" variant="tertiary" @click="isOpen = !isOpen">Toggle</Button>
</div>
</div>
<DrawerBackdrop :is-open="isOpen" @open-change="isOpen = $event">
<DrawerContent placement="right">
<DrawerDialog>
<DrawerCloseTrigger />
<DrawerHeader>
<DrawerHeading>Controlled with ref()</DrawerHeading>
</DrawerHeader>
<DrawerBody>
<p>
This drawer is controlled by a Vue <code>ref</code>. Pass <code>isOpen</code> and
listen for <code>openChange</code> to manage state externally.
</p>
</DrawerBody>
<DrawerFooter>
<Button data-drawer-close="true" variant="secondary">Close</Button>
</DrawerFooter>
</DrawerDialog>
</DrawerContent>
</DrawerBackdrop>
</div>
<div class="flex flex-col gap-3">
<h3 class="m-0 text-lg font-semibold text-foreground">With small state helpers</h3>
<p class="m-0 text-sm leading-relaxed text-pretty text-muted">
Use a tiny local helper object for convenient methods like <code>open()</code>,
<code>close()</code>, and <code>toggle()</code>.
</p>
<div class="flex flex-col items-start gap-3 rounded-2xl bg-surface p-4 shadow-sm">
<div class="flex w-full items-center justify-between">
<p class="m-0 text-xs text-muted">
Status:
<span class="font-mono font-medium text-foreground">
{{ overlay.isOpen.value ? 'open' : 'closed' }}
</span>
</p>
</div>
<div class="flex gap-2">
<Button size="sm" variant="secondary" @click="overlay.open">Open Drawer</Button>
<Button size="sm" variant="tertiary" @click="overlay.toggle">Toggle</Button>
</div>
</div>
<DrawerBackdrop :is-open="overlay.isOpen.value" @open-change="overlay.setOpen">
<DrawerContent placement="right">
<DrawerDialog>
<DrawerCloseTrigger />
<DrawerHeader>
<DrawerHeading>Controlled with helpers</DrawerHeading>
</DrawerHeader>
<DrawerBody>
<p>
The helper object provides dedicated methods for common operations without manually
creating callbacks every time.
</p>
</DrawerBody>
<DrawerFooter>
<Button data-drawer-close="true" variant="secondary">Close</Button>
</DrawerFooter>
</DrawerDialog>
</DrawerContent>
</DrawerBackdrop>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import {
Button,
DrawerBackdrop,
DrawerBody,
DrawerCloseTrigger,
DrawerContent,
DrawerDialog,
DrawerFooter,
DrawerHeader,
DrawerHeading,
} from '@rysinal/heroui-vue'
const isOpen = ref(false)
const helperOpen = ref(false)
const overlay = {
close: () => {
helperOpen.value = false
},
isOpen: computed(() => helperOpen.value),
open: () => {
helperOpen.value = true
},
setOpen: (value: boolean) => {
helperOpen.value = value
},
toggle: () => {
helperOpen.value = !helperOpen.value
},
}
</script>With Form
<template>
<Drawer>
<Button variant="secondary">Edit Profile</Button>
<DrawerBackdrop>
<DrawerContent placement="right">
<DrawerDialog>
<DrawerCloseTrigger />
<DrawerHeader>
<DrawerHeading>Edit Profile</DrawerHeading>
</DrawerHeader>
<DrawerBody>
<form class="flex flex-col gap-4" @submit.prevent>
<TextField
v-model="form.name"
class="w-full"
label="Name"
name="name"
placeholder="Enter your name"
variant="secondary"
/>
<TextField
v-model="form.email"
class="w-full"
label="Email"
name="email"
placeholder="Enter your email"
type="email"
variant="secondary"
/>
<TextField
v-model="form.bio"
class="w-full"
label="Bio"
name="bio"
placeholder="Tell us about yourself"
variant="secondary"
/>
</form>
</DrawerBody>
<DrawerFooter>
<Button data-drawer-close="true" variant="secondary">Cancel</Button>
<Button data-drawer-close="true">Save Changes</Button>
</DrawerFooter>
</DrawerDialog>
</DrawerContent>
</DrawerBackdrop>
</Drawer>
</template>
<script setup lang="ts">
import { reactive } from 'vue'
import {
Button,
Drawer,
DrawerBackdrop,
DrawerBody,
DrawerCloseTrigger,
DrawerContent,
DrawerDialog,
DrawerFooter,
DrawerHeader,
DrawerHeading,
TextField,
} from '@rysinal/heroui-vue'
const form = reactive({
bio: '',
email: '',
name: '',
})
</script>Navigation Drawer
<template>
<Drawer>
<Button variant="secondary">
<svg
aria-hidden="true"
class="size-5"
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
>
<path d="M4 6h16" />
<path d="M4 12h16" />
<path d="M4 18h16" />
</svg>
Menu
</Button>
<DrawerBackdrop>
<DrawerContent placement="left">
<DrawerDialog>
<DrawerCloseTrigger />
<DrawerHeader>
<DrawerHeading>Navigation</DrawerHeading>
</DrawerHeader>
<DrawerBody>
<nav aria-label="Main navigation" class="flex flex-col gap-1">
<button
v-for="item in navItems"
:key="item.label"
class="flex w-full items-center justify-start gap-3 rounded-xl px-3 py-2.5 text-left text-sm text-foreground transition-colors hover:bg-default"
type="button"
>
<svg
aria-hidden="true"
class="size-5 shrink-0 text-muted"
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
>
<component
:is="segment.element"
v-for="segment in item.icon"
:key="segment.key"
v-bind="segment.attrs"
/>
</svg>
{{ item.label }}
</button>
</nav>
</DrawerBody>
</DrawerDialog>
</DrawerContent>
</DrawerBackdrop>
</Drawer>
</template>
<script setup lang="ts">
import {
Button,
Drawer,
DrawerBackdrop,
DrawerBody,
DrawerCloseTrigger,
DrawerContent,
DrawerDialog,
DrawerHeader,
DrawerHeading,
} from '@rysinal/heroui-vue'
type IconSegment = {
attrs: Record<string, string | number>
element: 'circle' | 'path' | 'rect'
key: string
}
const navItems = [
{
icon: [
{ attrs: { d: 'M3 10.5 12 3l9 7.5' }, element: 'path', key: 'roof' },
{ attrs: { d: 'M5 10v10h14V10' }, element: 'path', key: 'frame' },
{ attrs: { d: 'M9 20v-6h6v6' }, element: 'path', key: 'door' },
],
label: 'Home',
},
{
icon: [
{ attrs: { cx: 11, cy: 11, r: 7 }, element: 'circle', key: 'lens' },
{ attrs: { d: 'm20 20-4.2-4.2' }, element: 'path', key: 'handle' },
],
label: 'Search',
},
{
icon: [
{ attrs: { d: 'M18 8a6 6 0 0 0-12 0c0 7-3 7-3 9h18c0-2-3-2-3-9' }, element: 'path', key: 'bell' },
{ attrs: { d: 'M13.7 21a2 2 0 0 1-3.4 0' }, element: 'path', key: 'clapper' },
],
label: 'Notifications',
},
{
icon: [
{ attrs: { height: 16, rx: 2, width: 20, x: 2, y: 4 }, element: 'rect', key: 'box' },
{ attrs: { d: 'm22 7-8.5 5.5a3 3 0 0 1-3 0L2 7' }, element: 'path', key: 'fold' },
],
label: 'Messages',
},
{
icon: [
{ attrs: { cx: 12, cy: 7, r: 4 }, element: 'circle', key: 'avatar' },
{ attrs: { d: 'M4 21v-2a6 6 0 0 1 6-6h4a6 6 0 0 1 6 6v2' }, element: 'path', key: 'body' },
],
label: 'Profile',
},
{
icon: [
{ attrs: { cx: 12, cy: 12, r: 3 }, element: 'circle', key: 'center' },
{
attrs: {
d: 'M19.4 15a1.8 1.8 0 0 0 .4 2l.1.1a2.1 2.1 0 0 1-3 3l-.1-.1a1.8 1.8 0 0 0-2-.4 1.8 1.8 0 0 0-1 1.7V21a2.1 2.1 0 0 1-4.2 0v-.1a1.8 1.8 0 0 0-1-1.7 1.8 1.8 0 0 0-2 .4l-.1.1a2.1 2.1 0 0 1-3-3l.1-.1a1.8 1.8 0 0 0 .4-2 1.8 1.8 0 0 0-1.7-1H3a2.1 2.1 0 0 1 0-4.2h.1a1.8 1.8 0 0 0 1.7-1 1.8 1.8 0 0 0-.4-2l-.1-.1a2.1 2.1 0 0 1 3-3l.1.1a1.8 1.8 0 0 0 2 .4 1.8 1.8 0 0 0 1-1.7V3a2.1 2.1 0 0 1 4.2 0v.1a1.8 1.8 0 0 0 1 1.7 1.8 1.8 0 0 0 2-.4l.1-.1a2.1 2.1 0 0 1 3 3l-.1.1a1.8 1.8 0 0 0-.4 2 1.8 1.8 0 0 0 1.7 1h.1a2.1 2.1 0 0 1 0 4.2h-.1a1.8 1.8 0 0 0-1.7 1z',
},
element: 'path',
key: 'gear',
},
],
label: 'Settings',
},
] satisfies Array<{ icon: IconSegment[]; label: string }>
</script>API
Drawer Props
| Prop | Type | Default | Description |
|---|---|---|---|
modelValue | boolean | undefined | Controlled open state. |
isOpen | boolean | undefined | Alternative controlled open state. |
defaultOpen | boolean | false | Initial uncontrolled open state. |
DrawerBackdrop Props
| Prop | Type | Default | Description |
|---|---|---|---|
variant | 'transparent' | 'opaque' | 'blur' | 'opaque' | Backdrop treatment. |
isDismissable | boolean | true | Close when pressing the backdrop or dragging past the dismiss threshold. |
isKeyboardDismissDisabled | boolean | false | Disable Escape dismissal. |
modelValue | boolean | undefined | Standalone controlled open state. |
isOpen | boolean | undefined | Standalone controlled open state alias. |
portalContainer | HTMLElement | string | 'body' | Custom teleport target. |
unstablePortalContainer | HTMLElement | undefined | React-compatible custom portal target alias. |
DrawerContent Props
| Prop | Type | Default | Description |
|---|---|---|---|
placement | 'bottom' | 'top' | 'left' | 'right' | 'bottom' | Drawer edge placement. |
DrawerHeading Props
| Prop | Type | Default | Description |
|---|---|---|---|
as | string | undefined | Rendered heading element override. |
level | 1 | 2 | 3 | 4 | 5 | 6 | 2 | Heading level semantics. |