Interactive

Drawer

A slide-in panel with animated content and optional handle.

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

Customize

Open
DirectionBottom
TitleDrawer
DescriptionFlip Open, then try the other props.
Inset
Overlay
Handle
Animated Handle
Dismissible
Modal
Should Scale Background

Props

Prop Type Default Description
openbooleanundefinedControlled open state. Use with v-model:open.
defaultOpenbooleanundefinedInitial open state when uncontrolled.
direction'bottom' | 'top' | 'left' | 'right''bottom'The direction from which the drawer slides in.
titlestringundefinedTitle text for the default header.
descriptionstringundefinedDescription text for the default header.
insetbooleanfalseWhether the drawer is inset (not full width/height).
overlaybooleantrueWhether to show the backdrop overlay.
handlebooleantrueWhether to show the drawer handle.
animatedHandlebooleantrueWhether to animate the handle with a pulsing effect (only applies when direction is 'bottom').
portalstring | boolean | HTMLElementtrueWhether to render the drawer in a portal.
nestedbooleanfalseWhether this is a nested drawer.
closeboolean | objectfalseWhether to show the close button.
closeIconstringundefinedIcon name for the close button.
dismissiblebooleantrueWhether the drawer can be dismissed by clicking overlay or pressing Escape.
modalbooleantrueWhether to trap focus in the drawer.
fixedbooleanundefinedWhether the drawer has fixed positioning.
handleOnlybooleanfalseWhether to only show the handle.
snapPoints(string | number)[]undefinedSnap points for the drawer (for bottom/top directions).
activeSnapPointstring | number | nullundefinedCurrently active snap point.
closeThresholdnumberundefinedThreshold for closing the drawer when dragging.
shouldScaleBackgroundbooleanundefinedWhether to scale the background when drawer is open (bottom direction).
setBackgroundColorOnScalebooleantrueWhether to set background color when scaling.
noBodyStylesbooleanundefinedWhether to prevent modifying body styles.
preventScrollRestorationbooleanundefinedWhether to prevent scroll restoration.
uiRecord<string, any>() => ({})Custom UI configuration passed to UDrawer.

Events

Event Payload Description
update:open(value: boolean) => voidEmitted when the open state changes.
close() => voidEmitted when the drawer is closed.
drag(percentageDragged: number) => voidEmitted during dragging.
release(open: boolean) => voidEmitted on drag release.
animationEnd(open: boolean) => voidEmitted when animations end.
update:activeSnapPoint(val: string | number) => voidEmitted 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 }) => voidCustom 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
uiobject-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 AnimatePresence for 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.
Set animatedHandle to false if you prefer a static handle. For side drawers, the handle is hidden regardless of this prop.
Copyright © 2026