Usage
Use aButton 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 thetitle
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 thedescription
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 thedirection
prop to control the direction of the Drawer. Defaults tobottom
.
<template> <UDrawer direction="right"> <UButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide-chevron-up" /> <template #content> <Placeholder class="min-w-96 min-h-96 size-full m-4" /> </template> </UDrawer></template>
Inset
Use theinset
prop to inset the Drawer from the edges.
<template> <UDrawer direction="right" inset> <UButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide-chevron-up" /> <template #content> <Placeholder class="min-w-96 min-h-96 size-full m-4" /> </template> </UDrawer></template>
Handle
Use thehandle
prop to control whether the Drawer has a handle or not. Defaults totrue
.
<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>
Handle Only
Use thehandle-only
prop to only allow the Drawer to be dragged by the handle.
<template> <UDrawer handle-only> <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 theoverlay
prop to control whether the Drawer has an overlay or not. Defaults totrue
.
<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 theshould-scale-background
prop to scale the background when the Drawer is open, creating a visual depth effect. You can set theset-background-color-on-scale
prop tofalse
to prevent changing the background color.
<template> <UDrawer should-scale-background set-background-color-on-scale> <UButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide-chevron-up" /> <template #content> <Placeholder class="h-48 m-4" /> </template> </UDrawer></template>
data-vaul-drawer-wrapper
directive to a parent element of your app to make this work.<template> <UApp> <div class="bg-default" data-vaul-drawer-wrapper> <NuxtLayout> <NuxtPage /> </NuxtLayout> </div> </UApp></template>
export default defineNuxtConfig({ app: { rootAttrs: { 'data-vaul-drawer-wrapper': '', 'class': 'bg-default' } }})
Examples
Control open state
You can control the open state by using thedefault-open
prop or thev-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>
defineShortcuts
, you can toggle the Drawer by pressingO.Disable dismissal
Set thedismissible
prop tofalse
to prevent the Drawer from being closed when clicking outside of it or pressing escape.
<script setup lang="ts">const open= ref(false)</script><template> <UDrawer v-model:open="open" :dismissible="false" :handle="false" :ui="{ header: 'flex items-center justify-between' }" > <UButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide-chevron-up" /> <template #header> <h2 class="text-highlighted font-semibold">Drawer non-dismissible</h2> <UButton color="neutral" variant="ghost" icon="i-lucide-x" @click="open = false" /> </template> <template #body> <Placeholder class="h-48" /> </template> </UDrawer></template>
header
slot is used to add a close button which is not done by default.With interactive background
Set theoverlay
andmodal
props tofalse
alongside thedismissible
prop to make the Drawer's background interactive without closing the Drawer.
<script setup lang="ts">const open= ref(false)</script><template> <UDrawer v-model:open="open" :dismissible="false" :overlay="false" :handle="false" :modal="false" :ui="{ header: 'flex items-center justify-between' }" > <UButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide-chevron-up" /> <template #header> <h2 class="text-highlighted font-semibold">Drawer non-dismissible</h2> <UButton color="neutral" variant="ghost" icon="i-lucide-x" @click="open = false" /> </template> <template #body> <Placeholder class="h-48" /> </template> </UDrawer></template>
Responsive drawer
You can render aModal component on desktop and a Drawer on mobile for example.
<script lang="ts" setup>import { createReusableTemplate, useMediaQuery } from '@vueuse/core'const [DefineFormTemplate, ReuseFormTemplate] = createReusableTemplate()const isDesktop= useMediaQuery('(min-width: 768px)')const open= ref(false)const state= reactive({ email: undefined})const title= 'Edit profile'const description= "Make changes to your profile here. Click save when you're done."</script><template> <DefineFormTemplate> <UForm :state="state" class="space-y-4"> <UFormField label="Email" name="email" required> <UInput v-model="state.email" placeholder="[email protected]" required /> </UFormField> <UButton label="Save changes" type="submit" /> </UForm> </DefineFormTemplate> <UModal v-if="isDesktop" v-model:open="open" :title="title" :description="description"> <UButton label="Edit profile" color="neutral" variant="outline" /> <template #body> <ReuseFormTemplate /> </template> </UModal> <UDrawer v-else v-model:open="open" :title="title" :description="description"> <UButton label="Edit profile" color="neutral" variant="outline" /> <template #body> <ReuseFormTemplate /> </template> </UDrawer></template>
With footer slot
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 aCommandPalette component inside the Drawer's content.
<script setup lang="ts">const searchTerm= ref('')const { data: users, status} = await useFetch('https://jsonplaceholder.typicode.com/users', { key: 'command-palette-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|| [], ignoreFilter: true}])</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 |
|
The element or component this component should render as. |
title |
| |
description |
| |
inset |
|
Whether to inset the drawer from the edges. |
content |
The content of the drawer.
| |
overlay |
|
Render an overlay behind the drawer. |
handle |
|
Render a handle on the drawer. |
portal |
|
Render the drawer in a portal. |
fixed |
When | |
open |
| |
defaultOpen |
Opened by default, skips initial enter animation. Still reacts to | |
activeSnapPoint |
| |
closeThreshold |
Number between 0 and 1 that determines when the drawer should be closed.Example: threshold of 0.5 would close the drawer if the user swiped for 50% of the height of the drawer or more. | |
shouldScaleBackground |
| |
setBackgroundColorOnScale |
When | |
scrollLockTimeout |
Duration for which the drawer is not draggable after scrolling content inside of the drawer. | |
dismissible |
|
When |
modal |
|
When |
nested |
| |
direction |
|
Direction of the drawer. Can be |
noBodyStyles |
When | |
handleOnly |
When | |
preventScrollRestoration |
| |
snapPoints |
Array of numbers from 0 to 100 that corresponds to % of the screen a given snap point should take up.Should go from least visible. Example | |
ui |
|
Slots
Slot | Type |
---|---|
default |
|
content |
|
header |
|
title |
|
description |
|
body |
|
footer |
|
Emits
Event | Type |
---|---|
close |
|
drag |
|
release |
|
update:open |
|
update:activeSnapPoint |
|
animationEnd |
|
Theme
export default defineAppConfig({ ui: { drawer: { slots: { overlay: 'fixed inset-0 bg-elevated/75', content: 'fixed bg-default ring ring-default flex focus:outline-none', handle: [ 'shrink-0 !bg-accented', 'transition-opacity' ], container: 'w-full flex flex-col gap-4 p-4 overflow-y-auto', header: '', title: 'text-highlighted font-semibold', description: 'mt-1 text-muted text-sm', body: 'flex-1', footer: 'flex flex-col gap-1.5' }, variants: { direction: { top: { content: 'mb-24 flex-col-reverse', handle: 'mb-4' }, right: { content: 'flex-row', handle: '!ml-4' }, bottom: { content: 'mt-24 flex-col', handle: 'mt-4' }, left: { content: 'flex-row-reverse', handle: '!mr-4' } }, inset: { true: { content: 'rounded-lg after:hidden overflow-hidden' } } }, compoundVariants: [ { direction: [ 'top', 'bottom' ], class: { content: 'h-auto max-h-[96%]', handle: '!w-12 !h-1.5 mx-auto' } }, { direction: [ 'right', 'left' ], class: { content: 'w-auto max-w-[calc(100%-2rem)]', handle: '!h-12 !w-1.5 mt-auto mb-auto' } }, { direction: 'top', inset: true, class: { content: 'inset-x-4 top-4' } }, { direction: 'top', inset: false, class: { content: 'inset-x-0 top-0 rounded-b-lg' } }, { direction: 'bottom', inset: true, class: { content: 'inset-x-4 bottom-4' } }, { direction: 'bottom', inset: false, class: { content: 'inset-x-0 bottom-0 rounded-t-lg' } }, { direction: 'left', inset: true, class: { content: 'inset-y-4 left-4' } }, { direction: 'left', inset: false, class: { content: 'inset-y-0 left-0 rounded-r-lg' } }, { direction: 'right', inset: true, class: { content: 'inset-y-4 right-4' } }, { direction: 'right', inset: false, class: { content: 'inset-y-0 right-0 rounded-l-lg' } } ] } }})
import { defineConfig } from 'vite'import vuefrom '@vitejs/plugin-vue'import uifrom '@nuxt/ui/vite'export default defineConfig({ plugins: [ vue(), ui({ ui: { drawer: { slots: { overlay: 'fixed inset-0 bg-elevated/75', content: 'fixed bg-default ring ring-default flex focus:outline-none', handle: [ 'shrink-0 !bg-accented', 'transition-opacity' ], container: 'w-full flex flex-col gap-4 p-4 overflow-y-auto', header: '', title: 'text-highlighted font-semibold', description: 'mt-1 text-muted text-sm', body: 'flex-1', footer: 'flex flex-col gap-1.5' }, variants: { direction: { top: { content: 'mb-24 flex-col-reverse', handle: 'mb-4' }, right: { content: 'flex-row', handle: '!ml-4' }, bottom: { content: 'mt-24 flex-col', handle: 'mt-4' }, left: { content: 'flex-row-reverse', handle: '!mr-4' } }, inset: { true: { content: 'rounded-lg after:hidden overflow-hidden' } } }, compoundVariants: [ { direction: [ 'top', 'bottom' ], class: { content: 'h-auto max-h-[96%]', handle: '!w-12 !h-1.5 mx-auto' } }, { direction: [ 'right', 'left' ], class: { content: 'w-auto max-w-[calc(100%-2rem)]', handle: '!h-12 !w-1.5 mt-auto mb-auto' } }, { direction: 'top', inset: true, class: { content: 'inset-x-4 top-4' } }, { direction: 'top', inset: false, class: { content: 'inset-x-0 top-0 rounded-b-lg' } }, { direction: 'bottom', inset: true, class: { content: 'inset-x-4 bottom-4' } }, { direction: 'bottom', inset: false, class: { content: 'inset-x-0 bottom-0 rounded-t-lg' } }, { direction: 'left', inset: true, class: { content: 'inset-y-4 left-4' } }, { direction: 'left', inset: false, class: { content: 'inset-y-0 left-0 rounded-r-lg' } }, { direction: 'right', inset: true, class: { content: 'inset-y-4 right-4' } }, { direction: 'right', inset: false, class: { content: 'inset-y-0 right-0 rounded-l-lg' } } ] } } }) ]})
import { defineConfig } from 'vite'import vuefrom '@vitejs/plugin-vue'import uiProfrom '@nuxt/ui-pro/vite'export default defineConfig({ plugins: [ vue(), uiPro({ ui: { drawer: { slots: { overlay: 'fixed inset-0 bg-elevated/75', content: 'fixed bg-default ring ring-default flex focus:outline-none', handle: [ 'shrink-0 !bg-accented', 'transition-opacity' ], container: 'w-full flex flex-col gap-4 p-4 overflow-y-auto', header: '', title: 'text-highlighted font-semibold', description: 'mt-1 text-muted text-sm', body: 'flex-1', footer: 'flex flex-col gap-1.5' }, variants: { direction: { top: { content: 'mb-24 flex-col-reverse', handle: 'mb-4' }, right: { content: 'flex-row', handle: '!ml-4' }, bottom: { content: 'mt-24 flex-col', handle: 'mt-4' }, left: { content: 'flex-row-reverse', handle: '!mr-4' } }, inset: { true: { content: 'rounded-lg after:hidden overflow-hidden' } } }, compoundVariants: [ { direction: [ 'top', 'bottom' ], class: { content: 'h-auto max-h-[96%]', handle: '!w-12 !h-1.5 mx-auto' } }, { direction: [ 'right', 'left' ], class: { content: 'w-auto max-w-[calc(100%-2rem)]', handle: '!h-12 !w-1.5 mt-auto mb-auto' } }, { direction: 'top', inset: true, class: { content: 'inset-x-4 top-4' } }, { direction: 'top', inset: false, class: { content: 'inset-x-0 top-0 rounded-b-lg' } }, { direction: 'bottom', inset: true, class: { content: 'inset-x-4 bottom-4' } }, { direction: 'bottom', inset: false, class: { content: 'inset-x-0 bottom-0 rounded-t-lg' } }, { direction: 'left', inset: true, class: { content: 'inset-y-4 left-4' } }, { direction: 'left', inset: false, class: { content: 'inset-y-0 left-0 rounded-r-lg' } }, { direction: 'right', inset: true, class: { content: 'inset-y-4 right-4' } }, { direction: 'right', inset: false, class: { content: 'inset-y-0 right-0 rounded-l-lg' } } ] } } }) ]})