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>
<PDrawer>
<PButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide:chevron-up" />
<template #content>
<CorePlaceholder class="h-48 m-4" />
</template>
</PDrawer>
</template>
You can also use the #header, #body and #footer slots to customize the Drawer's content.
Use the title prop to set the title of the Drawer's header.
<template>
<PDrawer title="Drawer with title">
<PButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide:chevron-up" />
<template #body>
<CorePlaceholder class="h-48" />
</template>
</PDrawer>
</template>
Use the description prop to set the description of the Drawer's header.
<template>
<PDrawer
title="Drawer with description"
description="Lorem ipsum dolor sit amet, consectetur adipiscing elit."
>
<PButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide:chevron-up" />
<template #body>
<CorePlaceholder class="h-48" />
</template>
</PDrawer>
</template>
Use the direction prop to control the direction of the Drawer. Defaults to bottom.
<template>
<PDrawer direction="right">
<PButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide:chevron-up" />
<template #content>
<CorePlaceholder class="min-w-96 min-h-96 size-full m-4" />
</template>
</PDrawer>
</template>
Use the inset prop to inset the Drawer from the edges.
<template>
<PDrawer direction="right" inset>
<PButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide:chevron-up" />
<template #content>
<CorePlaceholder class="min-w-96 min-h-96 size-full m-4" />
</template>
</PDrawer>
</template>
Use the handle prop to control whether the Drawer has a handle or not. Defaults to true.
<template>
<PDrawer>
<PButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide:chevron-up" />
<template #content>
<CorePlaceholder class="h-48 m-4" />
</template>
</PDrawer>
</template>
Use the handle-only prop to only allow the Drawer to be dragged by the handle.
<template>
<PDrawer handle-only>
<PButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide:chevron-up" />
<template #content>
<CorePlaceholder class="h-48 m-4" />
</template>
</PDrawer>
</template>
Use the overlay prop to control whether the Drawer has an overlay or not. Defaults to true.
<template>
<PDrawer>
<PButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide:chevron-up" />
<template #content>
<CorePlaceholder class="h-48 m-4" />
</template>
</PDrawer>
</template>
Use the modal prop to control whether the Drawer blocks interaction with outside content. Defaults to true.
modal is set to false, the overlay is automatically disabled and outside content becomes interactive.<template>
<PDrawer>
<PButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide:chevron-up" />
<template #content>
<CorePlaceholder class="h-48 m-4" />
</template>
</PDrawer>
</template>
Use the dismissible prop to control whether the Drawer is dismissible when clicking outside of it or pressing escape. Defaults to true.
close:prevent event will be emitted when the user tries to close it.modal: false with dismissible: false to make the Drawer's background interactive without closing it.<script setup lang="ts">
import { ref } from 'vue'
const open = ref(false)
</script>
<template>
<PDrawer
v-model:open="open"
:dismissible="false"
:handle="false"
:pohon="{ header: 'flex items-center justify-between' }"
>
<PButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide:chevron-up" />
<template #header>
<h2 class="text-highlighted font-semibold">Drawer non-dismissible</h2>
<PButton color="neutral" variant="ghost" icon="i-lucide:x" @click="open = false" />
</template>
<template #body>
<CorePlaceholder class="h-48" />
</template>
</PDrawer>
</template>
Use the should-scale-background prop to scale the background when the Drawer is open, creating a visual depth effect. You can set the set-background-color-on-scale prop to false to prevent changing the background color.
<template>
<PDrawer should-scale-background set-background-color-on-scale>
<PButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide:chevron-up" />
<template #content>
<CorePlaceholder class="h-48 m-4" />
</template>
</PDrawer>
</template>
data-vaul-drawer-wrapper directive to a parent element of your app to make this work.<template>
<PApp>
<div
class="bg-background"
data-vaul-drawer-wrapper
>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</div>
</PApp>
</template>
export default defineNuxtConfig({
app: {
rootAttrs: {
'data-vaul-drawer-wrapper': '',
'class': 'bg-background'
}
}
});
You can control the open state by using the default-open prop or the v-model:open directive.
<script setup lang="ts">
import { defineShortcuts } from '#imports'
import { ref } from 'vue'
const open = ref(false)
defineShortcuts({
o: () => {
open.value = !open.value
}
})
</script>
<template>
<PDrawer v-model:open="open">
<PButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide:chevron-up" />
<template #content>
<CorePlaceholder class="m-4 h-48" />
</template>
</PDrawer>
</template>
defineShortcuts, you can toggle the Drawer by pressing O.You can render a Dialog component on desktop and a Drawer on mobile for example.
<script lang="ts" setup>
import { createReusableTemplate, useMediaQuery } from '@vueuse/core'
import { reactive, ref } from 'vue'
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>
<PForm :state="state" class="space-y-4">
<PFormField label="Email" name="email" required>
<PInput v-model="state.email" placeholder="shadcn@example.com" required />
</PFormField>
<PButton label="Save changes" type="submit" />
</PForm>
</DefineFormTemplate>
<PDialog v-if="isDesktop" v-model:open="open" :title="title" :description="description">
<PButton label="Edit profile" color="neutral" variant="outline" />
<template #body>
<ReuseFormTemplate />
</template>
</PDialog>
<PDrawer v-else v-model:open="open" :title="title" :description="description">
<PButton label="Edit profile" color="neutral" variant="outline" />
<template #body>
<ReuseFormTemplate />
</template>
</PDrawer>
</template>
You can nest drawers within each other by using the nested prop.
<template>
<PDrawer :pohon="{ content: 'h-full', overlay: 'bg-inverted/30' }">
<PButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide:chevron-up" />
<template #footer>
<PDrawer nested :pohon="{ content: 'h-full', overlay: 'bg-inverted/30' }">
<PButton color="neutral" variant="outline" label="Open nested" />
<template #content>
<CorePlaceholder class="m-4 flex-1" />
</template>
</PDrawer>
</template>
</PDrawer>
</template>
Use the #footer slot to add content after the Drawer's body.
<script setup lang="ts">
import { ref } from 'vue'
const open = ref(false)
</script>
<template>
<PDrawer
v-model:open="open"
title="Drawer with footer"
description="This is useful when you want a form in a Drawer."
:pohon="{ container: 'max-w-xl mx-auto' }"
>
<PButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide:chevron-up" />
<template #body>
<CorePlaceholder class="h-48" />
</template>
<template #footer>
<PButton label="Submit" color="neutral" class="justify-center" />
<PButton
label="Cancel"
color="neutral"
variant="outline"
class="justify-center"
@click="open = false"
/>
</template>
</PDrawer>
</template>
You can use a CommandPalette component inside the Drawer's content.
<script setup lang="ts">
import { useFetch } from '#app';
import { computed, ref } from 'vue';
const searchTerm = ref('');
const { data: users, status } = await useFetch('https://jsonplaceholder.typicode.com/users', {
key: 'command-palette-users',
params: { q: searchTerm },
transform: (data: Array<{ 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>
<PDrawer :handle="false">
<PButton
label="Search users..."
color="neutral"
variant="subtle"
icon="i-lucide:search"
/>
<template #content>
<PCommandPalette
v-model:search-term="searchTerm"
:loading="status === 'pending'"
:groups="groups"
placeholder="Search users..."
class="h-80"
/>
</template>
</PDrawer>
</template>
| Prop | Default | Type |
|---|
| Slot | Type |
|---|
| Event | Type |
|---|
Below is the theme configuration skeleton for the PDrawer. Since the component is provided unstyled by default, you will need to fill in these values to apply your own custom look and feel. If you prefer to use our pre-built, opinionated styling, you can instead use our UnoCSS preset, this docs is using it as well.
export default defineAppConfig({
pohon: {
drawer: {
slots: {
overlay: '',
content: '',
handle: '',
container: '',
header: '',
title: '',
description: '',
body: '',
footer: ''
},
variants: {
direction: {
top: {
content: '',
handle: ''
},
right: {
content: '',
handle: ''
},
bottom: {
content: '',
handle: ''
},
left: {
content: '',
handle: ''
}
},
inset: {
true: {
content: ''
}
},
snapPoints: {
true: ''
}
},
compoundVariants: []
}
}
};
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import pohon from 'pohon-ui/vite'
export default defineAppConfig({
pohon: {
drawer: {
slots: {
overlay: '',
content: '',
handle: '',
container: '',
header: '',
title: '',
description: '',
body: '',
footer: ''
},
variants: {
direction: {
top: {
content: '',
handle: ''
},
right: {
content: '',
handle: ''
},
bottom: {
content: '',
handle: ''
},
left: {
content: '',
handle: ''
}
},
inset: {
true: {
content: ''
}
},
snapPoints: {
true: ''
}
},
compoundVariants: []
}
}
};