Drawer
A slide-in drawer built on Nuxt UI's UDrawer with animated entrance/exit transitions and staggered child animations. The drawer slides in from the specified direction and animates its header, body, and footer content with motion effects.
Usage
Use v-model:open to control the drawer's visibility. You can provide title and description for a default header, or use the header slot for custom content. The body slot contains the main content.
<template>
<div class="flex w-full max-w-md justify-center">
<div class="flex flex-col gap-3">
<UButton
label="Open Drawer"
@click="open = true"
/>
<MDrawer
v-model:open="open"
title="Drawer"
description="A simple drawer example"
>
<template #body>
<div class="flex flex-col gap-2">
<p class="text-sm text-muted">
This is the drawer body content.
</p>
<UButton
label="Close"
@click="open = false"
/>
</div>
</template>
</MDrawer>
</div>
</div>
</template>
<script setup lang="ts">
const open = ref(false)
</script>
Direction
The direction prop slides the panel in from any edge. The drag handle is only rendered for the bottom direction, so the other directions show a plain edge.
<template>
<div class="flex w-full flex-col items-center gap-4">
<div class="flex flex-wrap justify-center gap-3">
<UButton
v-for="item in directions"
:key="item.value"
:label="item.label"
color="neutral"
variant="soft"
@click="openFrom(item.value)"
/>
</div>
<MDrawer
v-model:open="open"
:direction="direction"
:title="`${activeLabel} drawer`"
:description="`This panel slides in from the ${direction}.`"
>
<template #body>
<div class="flex flex-col gap-3">
<p class="text-sm text-muted">
One component, four directions. Choose another button to watch the
panel slide in from a different edge.
</p>
<UButton
label="Close"
color="neutral"
variant="soft"
@click="open = false"
/>
</div>
</template>
</MDrawer>
</div>
</template>
<script setup lang="ts">
type Direction = 'bottom' | 'top' | 'left' | 'right'
const directions = [
{ label: 'Bottom', value: 'bottom' as Direction },
{ label: 'Top', value: 'top' as Direction },
{ label: 'Left', value: 'left' as Direction },
{ label: 'Right', value: 'right' as Direction },
]
const open = ref(false)
const direction = ref<Direction>('bottom')
const activeLabel = computed(
() => directions.find(item => item.value === direction.value)?.label ?? '',
)
const openFrom = (value: Direction) => {
direction.value = value
open.value = true
}
</script>
Customize
Toggle Open to reveal the drawer. This paragraph is the drawer's default slot content.
Customize
Props
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | undefined | Controlled open state. Use with v-model:open. |
defaultOpen | boolean | undefined | Initial open state when uncontrolled. |
direction | 'bottom' | 'top' | 'left' | 'right' | 'bottom' | The direction from which the drawer slides in. |
title | string | undefined | Title text for the default header. |
description | string | undefined | Description text for the default header. |
inset | boolean | false | Whether the drawer is inset (not full width/height). |
overlay | boolean | true | Whether to show the backdrop overlay. |
handle | boolean | true | Whether to show the drawer handle. |
animatedHandle | boolean | true | Whether to animate the handle with a pulsing effect (only applies when direction is 'bottom'). |
portal | string | boolean | HTMLElement | true | Whether to render the drawer in a portal. |
nested | boolean | false | Whether this is a nested drawer. |
close | boolean | object | false | Whether to show the close button. |
closeIcon | string | undefined | Icon name for the close button. |
dismissible | boolean | true | Whether the drawer can be dismissed by clicking overlay or pressing Escape. |
modal | boolean | true | Whether to trap focus in the drawer. |
fixed | boolean | undefined | Whether the drawer has fixed positioning. |
handleOnly | boolean | false | Whether to only show the handle. |
snapPoints | (string | number)[] | undefined | Snap points for the drawer (for bottom/top directions). |
activeSnapPoint | string | number | null | undefined | Currently active snap point. |
closeThreshold | number | undefined | Threshold for closing the drawer when dragging. |
shouldScaleBackground | boolean | undefined | Whether to scale the background when drawer is open (bottom direction). |
setBackgroundColorOnScale | boolean | true | Whether to set background color when scaling. |
noBodyStyles | boolean | undefined | Whether to prevent modifying body styles. |
preventScrollRestoration | boolean | undefined | Whether to prevent scroll restoration. |
ui | Record<string, any> | () => ({}) | Custom UI configuration passed to UDrawer. |
Events
| Event | Payload | Description |
|---|---|---|
update:open | (value: boolean) => void | Emitted when the open state changes. |
close | () => void | Emitted when the drawer is closed. |
drag | (percentageDragged: number) => void | Emitted during dragging. |
release | (open: boolean) => void | Emitted on drag release. |
animationEnd | (open: boolean) => void | Emitted when animations end. |
update:activeSnapPoint | (val: string | number) => void | Emitted when active snap point changes. |
Slots
| Slot | Props | Description |
|---|---|---|
default | - | Default slot content inside the drawer. |
header | - | Custom header content. Overrides title/description. |
title | - | Custom title slot. |
description | - | Custom description slot. |
actions | - | Actions slot (typically for buttons). |
body | - | Main body content of the drawer. |
footer | - | Footer content (typically for actions). |
close | (props: { ui: any }) => void | Custom close button slot. |
UI
This component's theme exposes the slots below. Override any of them per instance with the ui prop, or app-wide under ui.drawer in app.config.ts.
| Prop | Type | Default | Description |
|---|---|---|---|
ui | object | - | Per-slot classes merged over the theme defaults. |
| Slot | Props | Description |
|---|---|---|
root | - | |
overlay | - | bg-default/50 backdrop-blur-sm |
content | - | flex flex-1 flex-col |
handle | - | mx-auto mt-4 h-1.5 w-12 shrink-0 rounded-full bg-accented |
header | - | px-6 py-4 border-b border-default |
body | - | flex-1 overflow-y-auto px-6 py-4 |
footer | - | px-6 py-4 border-t border-default |
headerContent | - | flex flex-col gap-1 |
title | - | text-lg font-semibold text-highlighted |
description | - | text-sm text-muted |
Accessibility
The drawer uses Nuxt UI's UDrawer under the hood, which provides proper focus trapping (when modal is true), ARIA attributes, and keyboard support (Escape to dismiss when dismissible). The animated handle is marked aria-hidden="true" as a decorative element. Content animations respect prefers-reduced-motion via useReducedMotion() - when reduced motion is preferred, opacity transitions are skipped or made instant while preserving functionality.
Performance
- Uses
AnimatePresencefor enter/exit animations with reduced-motion-aware transitions. - Content (header, body, footer) animates with staggered delays (base 120ms + 50ms per child) for a polished entrance, but disabled under reduced motion.
- Handle pulses with a 2-second loop when animated and direction is bottom; disabled under reduced motion.
- Overlay fade duration is 200ms (or 0 under reduced motion).
- Transforms are used sparingly; child animations use small y-offsets for natural motion.
- All motion values are computed reactively with
useReducedMotion()checks to avoid unnecessary animations for accessibility.
animatedHandle to false if you prefer a static handle. For side drawers, the handle is hidden regardless of this prop.