用法
在抽屉的默认插槽中使用Button或任何其他组件。
然后,使用#content插槽添加抽屉打开时显示的内容。
<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>您还可以使用#header、#body和#footer插槽来自定义抽屉的内容。
标题
使用title属性设置Drawer的标题。
<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属性设置Drawer的标题的描述。
<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>关闭:badge{label="4.10+" class="align-text-top"}
使用close属性在false的Drawer.exe中显示一个关闭按钮。
您可以从Button组件传递任何属性来对其进行自定义。
<template>
<UDrawer
title="Drawer with close button"
:close="{
color: 'primary',
variant: 'outline',
class: 'rounded-full'
}"
>
<UButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide-chevron-up" />
<template #body>
<Placeholder class="h-48" />
</template>
</UDrawer>
</template>关闭图标:badge{label="4.10+" class="align-text-top"}
使用close-icon道具自定义关闭按钮Icon.i-lucide-x。
<template>
<UDrawer title="Drawer with close button" close close-icon="i-lucide-arrow-right">
<UButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide-chevron-up" />
<template #body>
<Placeholder class="h-48" />
</template>
</UDrawer>
</template>方向
使用direction道具来控制抽屉的方向。
<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
使用inset道具从边缘插入抽屉。
<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属性来控制抽屉是否有句柄。将其转换为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>仅手柄
使用handle-only属性仅允许通过手柄拖动抽屉。
<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属性来控制抽屉是否有覆盖。将其转换为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>Modal
使用modal属性来控制抽屉是否阻止与外部内容的交互。
modal设置为false时,叠加将自动禁用,外部内容将变得交互式。<template>
<UDrawer :modal="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>可忽略
使用dismissible属性来控制当在抽屉外单击或按escape. true时抽屉是否被禁用。
close:prevent事件。modal: false与dismissible: false结合使用,使抽屉的背景具有交互性,而无需关闭它。<script setup lang="ts">
const open = ref(false)
</script>
<template>
<UDrawer
v-model:open="open"
title="Drawer non-dismissible"
close
:dismissible="false"
:modal="false"
:handle="false"
>
<UButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide-chevron-up" />
<template #body>
<Placeholder class="size-full min-h-48" />
</template>
</UDrawer>
</template><script setup lang="ts">
import { ref } from 'vue'
const open = ref(false)
</script>
<template>
<UDrawer
v-model:open="open"
title="Drawer non-dismissible"
close
:dismissible="false"
:modal="false"
:handle="false"
>
<UButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide-chevron-up" />
<template #body>
<Placeholder class="size-full min-h-48" />
</template>
</UDrawer>
</template>Scale背景
当抽屉打开时,使用should-scale-background道具缩放背景,创建视觉深度效果。您可以将set-background-color-on-scale道具设置为false,以防止更改背景颜色。
<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指令添加到应用的父元素中以使其工作。<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'
}
}
})
示例
控制打开状态
您可以使用default-open prop或v-model:open指令控制打开状态。
<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><script setup lang="ts">
import { ref } from 'vue'
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,您可以通过按:kbd{value="O"}来切换抽屉。响应式抽屉
例如,您可以在桌面上渲染Modal组件,在移动的上渲染Drawer。
<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="shadcn@example.com" 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>嵌套抽屉
您可以使用nested属性嵌套抽屉。
<template>
<UDrawer :ui="{ content: 'h-full', overlay: 'bg-inverted/30' }">
<UButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide-chevron-up" />
<template #footer>
<UDrawer nested :ui="{ content: 'h-full', overlay: 'bg-inverted/30' }">
<UButton color="neutral" variant="outline" label="Open nested" />
<template #content>
<Placeholder class="flex-1 m-4" />
</template>
</UDrawer>
</template>
</UDrawer>
</template>带页脚插槽
使用#footer插槽在Drawer的主体之后添加内容。
<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><script setup lang="ts">
import { ref } from 'vue'
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>带命令调色板
您可以在Drawer的内容中使用CommandPalette组件。
<script setup lang="ts">
const searchTerm = ref('')
const { data: users, status, execute } = await useLazyFetch('https://jsonplaceholder.typicode.com/users', {
key: 'drawer-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}`, loading: 'lazy' as const } })) || []
},
immediate: false
})
const groups = computed(() => [{
id: 'users',
label: searchTerm.value ? `Users matching “${searchTerm.value}”...` : 'Users',
items: users.value || [],
ignoreFilter: true
}])
function onOpen() {
if (!users.value?.length) {
execute()
}
}
</script>
<template>
<UDrawer :handle="false" @update:open="onOpen">
<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>
<script setup lang="ts">
import { ref, computed } from 'vue'
const searchTerm = ref('')
const { data: users, status, execute } = await useLazyFetch('https://jsonplaceholder.typicode.com/users', {
key: 'drawer-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}`, loading: 'lazy' as const } })) || []
},
immediate: false
})
const groups = computed(() => [{
id: 'users',
label: searchTerm.value ? `Users matching “${searchTerm.value}”...` : 'Users',
items: users.value || [],
ignoreFilter: true
}])
function onOpen() {
if (!users.value?.length) {
execute()
}
}
</script>
<template>
<UDrawer :handle="false" @update:open="onOpen">
<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>
useLazyFetch和immediate: false,仅在Drawer打开时获取数据。API
Props
| Prop | Default | Type |
|---|---|---|
as | 'div' | anyThe element or component this component should render as. |
title | string | |
description | string | |
inset | false | boolean Whether to inset the drawer from the edges. |
content | DialogContentProps & Partial<EmitsToProps<DialogContentImplEmits>>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 | string | false | true | HTMLElementRender the drawer in a portal. |
nested | false | boolean Whether the drawer is nested in another drawer. |
close | false | boolean | Omit<ButtonProps, LinkPropsKeys> Display a close button to dismiss the drawer.
|
closeIcon | appConfig.ui.icons.close | anyThe icon displayed in the close button. |
activeSnapPoint | null | string | number | |
closeThreshold | numberNumber 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 | boolean | |
setBackgroundColorOnScale | boolean When | |
scrollLockTimeout | numberDuration for which the drawer is not draggable after scrolling content inside of the drawer. | |
fixed | boolean When | |
dismissible | true | boolean When |
modal | true | boolean When |
open | boolean | |
defaultOpen | boolean Opened by default, skips initial enter animation. Still reacts to | |
direction | 'bottom' | "bottom" | "top" | "left" | "right"Direction of the drawer. Can be |
noBodyStyles | boolean When | |
handleOnly | boolean When | |
preventScrollRestoration | boolean | |
snapPoints | (string | number)[]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 | { overlay?: SlotClass; content?: SlotClass; handle?: SlotClass; container?: SlotClass; header?: SlotClass; wrapper?: SlotClass; title?: SlotClass; description?: SlotClass; actions?: SlotClass; body?: SlotClass; footer?: SlotClass; close?: SlotClass; } |
Slots
| Slot | Type |
|---|---|
default | {} |
content | {} |
header | {} |
title | {} |
description | {} |
actions | {} |
close | { ui: object; } |
body | {} |
footer | {} |
Emits
| Event | Type |
|---|---|
close | [] |
drag | [percentageDragged: number] |
close:prevent | [] |
release | [open: boolean] |
update:open | [open: boolean] |
update:activeSnapPoint | [val: string | number] |
animationEnd | [open: boolean] |
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 ease-out'
],
container: 'w-full flex flex-col gap-4 p-4 overflow-y-auto',
header: 'flex items-center gap-1.5 min-h-8',
wrapper: 'min-w-0 flex-1',
title: 'text-highlighted font-semibold',
description: 'mt-1 text-muted text-sm',
actions: 'flex items-center gap-1.5 shrink-0 ms-auto',
body: 'flex-1',
footer: 'flex flex-col gap-1.5',
close: ''
},
variants: {
direction: {
top: {
content: 'mb-24 flex-col-reverse',
handle: 'mb-4'
},
right: {
content: 'flex-row rtl:flex-row-reverse',
handle: '!ml-4'
},
bottom: {
content: 'mt-24 flex-col',
handle: 'mt-4'
},
left: {
content: 'flex-row-reverse rtl:flex-row',
handle: '!mr-4'
}
},
inset: {
true: {
content: 'rounded-lg after:hidden overflow-hidden [--initial-transform:calc(100%+1.5rem)]'
}
},
snapPoints: {
true: ''
}
},
compoundVariants: [
{
direction: [
'top',
'bottom'
],
class: {
content: 'h-auto max-h-[96%]',
handle: '!w-12 !h-1.5 mx-auto'
}
},
{
direction: [
'top',
'bottom'
],
snapPoints: true,
class: {
content: 'h-full'
}
},
{
direction: [
'right',
'left'
],
class: {
content: 'w-auto max-w-[calc(100%-2rem)]',
handle: '!h-12 !w-1.5 mt-auto mb-auto'
}
},
{
direction: [
'right',
'left'
],
snapPoints: true,
class: {
content: 'w-full'
}
},
{
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 vue from '@vitejs/plugin-vue'
import ui from '@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 ease-out'
],
container: 'w-full flex flex-col gap-4 p-4 overflow-y-auto',
header: 'flex items-center gap-1.5 min-h-8',
wrapper: 'min-w-0 flex-1',
title: 'text-highlighted font-semibold',
description: 'mt-1 text-muted text-sm',
actions: 'flex items-center gap-1.5 shrink-0 ms-auto',
body: 'flex-1',
footer: 'flex flex-col gap-1.5',
close: ''
},
variants: {
direction: {
top: {
content: 'mb-24 flex-col-reverse',
handle: 'mb-4'
},
right: {
content: 'flex-row rtl:flex-row-reverse',
handle: '!ml-4'
},
bottom: {
content: 'mt-24 flex-col',
handle: 'mt-4'
},
left: {
content: 'flex-row-reverse rtl:flex-row',
handle: '!mr-4'
}
},
inset: {
true: {
content: 'rounded-lg after:hidden overflow-hidden [--initial-transform:calc(100%+1.5rem)]'
}
},
snapPoints: {
true: ''
}
},
compoundVariants: [
{
direction: [
'top',
'bottom'
],
class: {
content: 'h-auto max-h-[96%]',
handle: '!w-12 !h-1.5 mx-auto'
}
},
{
direction: [
'top',
'bottom'
],
snapPoints: true,
class: {
content: 'h-full'
}
},
{
direction: [
'right',
'left'
],
class: {
content: 'w-auto max-w-[calc(100%-2rem)]',
handle: '!h-12 !w-1.5 mt-auto mb-auto'
}
},
{
direction: [
'right',
'left'
],
snapPoints: true,
class: {
content: 'w-full'
}
},
{
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'
}
}
]
}
}
})
]
})