A drawer that smoothly slides in & out of the screen.

Usage

Use a Button or any other component in the default slot of the Drawer.

Then, use the #content slot to add the content displayed when the Drawer is open.

<template>
  <UDrawer>
    <UButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide-chevron-up" />

    <template #content>
      <Placeholder class="h-48 m-4" />
    </template>
  </UDrawer>
</template>

You can also use the #header, #body and #footer slots to customize the Drawer's content.

Title

Use the title prop to set the title of the Drawer's header.

<template>
  <UDrawer title="Drawer with title">
    <UButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide-chevron-up" />

    <template #body>
      <Placeholder class="h-48" />
    </template>
  </UDrawer>
</template>

Description

Use the description prop to set the description of the Drawer's header.

<template>
  <UDrawer
    title="Drawer with description"
    description="Lorem ipsum dolor sit amet, consectetur adipiscing elit."
  >
    <UButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide-chevron-up" />

    <template #body>
      <Placeholder class="h-48" />
    </template>
  </UDrawer>
</template>

Direction

Use the direction prop to control the direction of the Drawer. Defaults to bottom.

<template>
  <UDrawer direction="top">
    <UButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide-chevron-up" />

    <template #content>
      <Placeholder class="h-96 m-4" />
    </template>
  </UDrawer>
</template>
<template>
  <UDrawer direction="right">
    <UButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide-chevron-up" />

    <template #content>
      <Placeholder class="w-96 m-4" />
    </template>
  </UDrawer>
</template>

Handle

Use the handle prop to control whether the Drawer has a handle or not. Defaults to true.

<template>
  <UDrawer :handle="false">
    <UButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide-chevron-up" />

    <template #content>
      <Placeholder class="h-48 m-4" />
    </template>
  </UDrawer>
</template>

Overlay

Use the overlay prop to control whether the Drawer has an overlay or not. Defaults to true.

<template>
  <UDrawer :overlay="false">
    <UButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide-chevron-up" />

    <template #content>
      <Placeholder class="h-48 m-4" />
    </template>
  </UDrawer>
</template>

Scale background

Use the should-scale-background prop to scale the background when the Drawer is open, creating a visual depth effect.

<template>
  <UDrawer should-scale-background>
    <UButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide-chevron-up" />

    <template #content>
      <Placeholder class="h-48 m-4" />
    </template>
  </UDrawer>
</template>
Make sure to add the vaul-drawer-wrapper directive to a parent element of your app to make this work.
app.vue
<template>
  <UApp>
    <div class="bg-[var(--ui-bg)]" vaul-drawer-wrapper>
      <NuxtLayout>
        <NuxtPage />
      </NuxtLayout>
    </div>
  </UApp>
</template>
nuxt.config.ts
export default defineNuxtConfig({
  app: {
    rootAttrs: {
      'vaul-drawer-wrapper': '',
      'class': 'bg-[var(--ui-bg)]'
    }
  }
})

Examples

Control open state

You can control the open state by using the default-open prop or the v-model:open directive.

<script setup lang="ts">
const open = ref(false)

defineShortcuts({
  o: () => (open.value = !open.value)
})
</script>

<template>
  <UDrawer v-model:open="open">
    <UButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide-chevron-up" />

    <template #content>
      <Placeholder class="h-48 m-4" />
    </template>
  </UDrawer>
</template>
In this example, leveraging defineShortcuts, you can toggle the Drawer by pressing O.
This allows you to move the trigger outside of the Drawer or remove it entirely.

Use the #footer slot to add content after the Drawer's body.

<script setup lang="ts">
const open = ref(false)
</script>

<template>
  <UDrawer
    v-model:open="open"
    title="Drawer with footer"
    description="This is useful when you want a form in a Drawer."
    :ui="{ container: 'max-w-xl mx-auto' }"
  >
    <UButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide-chevron-up" />

    <template #body>
      <Placeholder class="h-48" />
    </template>

    <template #footer>
      <UButton label="Submit" color="neutral" class="justify-center" />
      <UButton
        label="Cancel"
        color="neutral"
        variant="outline"
        class="justify-center"
        @click="open = false"
      />
    </template>
  </UDrawer>
</template>

With command palette

You can use a CommandPalette component inside the Drawer's content.

<script setup lang="ts">
const searchTerm = ref('')

const { data: users, status } = await useFetch('https://jsonplaceholder.typicode.com/users', {
  params: { q: searchTerm },
  transform: (data: { id: number, name: string, email: string }[]) => {
    return data?.map(user => ({ id: user.id, label: user.name, suffix: user.email, avatar: { src: `https://i.pravatar.cc/120?img=${user.id}` } })) || []
  },
  lazy: true
})

const groups = computed(() => [{
  id: 'users',
  label: searchTerm.value ? `Users matching “${searchTerm.value}”...` : 'Users',
  items: users.value || [],
  filter: false
}])
</script>

<template>
  <UDrawer :handle="false">
    <UButton
      label="Search users..."
      color="neutral"
      variant="subtle"
      icon="i-lucide-search"
    />

    <template #content>
      <UCommandPalette
        v-model:search-term="searchTerm"
        :loading="status === 'pending'"
        :groups="groups"
        placeholder="Search users..."
        class="h-80"
      />
    </template>
  </UDrawer>
</template>

API

Props

Prop Default Type
as

'div'

any

The element or component this component should render as.

title

string

description

string

content

DialogContentProps

The content of the drawer.

overlay

true

boolean

Render an overlay behind the drawer.

handle

true

boolean

Render a handle on the drawer.

portal

true

boolean

Render the drawer in a portal.

fixed

boolean

open

boolean

defaultOpen

boolean

direction

"bottom"

"left" | "right" | "top" | "bottom"

modal

boolean

activeSnapPoint

null | string | number

closeThreshold

number

dismissible

boolean

fadeFromIndex

number

nested

boolean

scrollLockTimeout

number

shouldScaleBackground

boolean

snapPoints

(string | number)[]

ui

Partial<{ overlay: string; content: string; handle: string; container: string; header: string; title: string; description: string; body: string; footer: string; }>

Slots

Slot Type
default

{}

handle

{}

content

{}

header

{}

title

{}

description

{}

body

{}

footer

{}

Emits

Event Type
close

[]

drag

[percentageDragged: number]

update:open

[open: boolean]

release

[open: boolean]

update:activeSnapPoint

[val: string | number]

Theme

app.config.ts
export default defineAppConfig({
  ui: {
    drawer: {
      slots: {
        overlay: 'fixed inset-0 bg-[var(--ui-bg-elevated)]/75',
        content: 'fixed bg-[var(--ui-bg)] ring ring-[var(--ui-border)] flex focus:outline-none',
        handle: 'shrink-0 rounded-full bg-[var(--ui-bg-accented)]',
        container: 'w-full flex flex-col gap-4 p-4 overflow-y-auto',
        header: '',
        title: 'text-[var(--ui-text-highlighted)] font-semibold',
        description: 'mt-1 text-[var(--ui-text-muted)] text-sm',
        body: 'flex-1',
        footer: 'flex flex-col gap-1.5'
      },
      variants: {
        direction: {
          top: {
            content: 'top-0 mb-24 flex-col-reverse rounded-b-[calc(var(--ui-radius)*2)]',
            handle: 'mb-4'
          },
          right: {
            content: 'right-4 flex-row',
            handle: 'ml-4'
          },
          bottom: {
            content: 'bottom-0 mt-24 flex-col rounded-t-[calc(var(--ui-radius)*2)]',
            handle: 'mt-4'
          },
          left: {
            content: 'left-4 flex-row-reverse',
            handle: 'mr-4'
          }
        }
      },
      compoundVariants: [
        {
          direction: [
            'top',
            'bottom'
          ],
          class: {
            content: 'inset-x-0 h-auto max-h-[96%]',
            handle: 'w-12 h-1.5 mx-auto'
          }
        },
        {
          direction: [
            'right',
            'left'
          ],
          class: {
            content: 'inset-y-4 w-auto max-w-[calc(100%-2rem)] rounded-[calc(var(--ui-radius)*2)] after:hidden',
            handle: 'h-12 w-1.5 mt-auto mb-auto'
          }
        }
      ]
    }
  }
})