使用法
DrawerのデフォルトスロットにあるButtonまたはその他のコンポーネントを使用します。
次に、#contentスロットを使用して、Drawerが開いたときに表示されるコンテンツを追加します。
<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スロットを使用してDrawerのコンテンツをカスタマイズすることもできます。
Title
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
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プロパティを使用して、Drawerに閉じるボタンを表示します。デフォルトはfalseです。
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>方向
Drawerの方向を制御するには、directionプロパティを使用します。デフォルトはbottomです。
<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プロパティを使用して、Drawerをエッジから挿入します。
<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>ハンドル
Drawerにハンドルがあるかどうかを制御するには、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
handle-onlyプロパティを使用して、Drawerをハンドルでのみドラッグできるようにします。
<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プロパティを使用して、Drawerにオーバーレイがあるかどうかを制御します。デフォルトは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プロパティを使用して、Drawerが外部コンテンツとのインタラクションをブロックするかどうかを制御します。デフォルトはtrueです。
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
dismissibleプロパティを使用して、Drawerの外側をクリックするかescapeを押したときにDrawerがdismissibleかどうかを制御します。デフォルトはtrueです。
close:preventイベントは、ユーザーがクローズしようとすると発行されます。modal: falseとdismissible: falseを組み合わせると、Drawerの背景を閉じずにインタラクティブにすることができます。<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プロパティを使用して、Drawerが開いているときに背景を拡大し、視覚的な奥行き効果を作成します。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'
}
}
})
例
Controlオープンステート
オープン状態はdefault-openプロパティまたは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"}を押してDrawerを切り替えることができます。Responsiveドロワー
例えば、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>Withコマンドパレット
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; } |
スロット
| 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'
}
}
]
}
}
})
]
})