画面の内外をスムーズにスライドさせる引き出し。

使用法

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プロパティを使用して、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ディレクティブを追加してください。
app.vue
<template>
  <UApp>
    <div class="bg-default" data-vaul-drawer-wrapper>
      <NuxtLayout>
        <NuxtPage />
      </NuxtLayout>
    </div>
  </UApp>
</template>
nuxt.config.ts
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'
any

The 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 | HTMLElement

Render 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. { size: 'md', color: 'neutral', variant: 'ghost' }

closeIcon
appConfig.ui.icons.close
any

The icon displayed in the close button.

activeSnapPoint
null | string | number
closeThreshold
number

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
boolean
setBackgroundColorOnScale
boolean

When false we don't change body's background color when the drawer is open.

scrollLockTimeout
number

Duration for which the drawer is not draggable after scrolling content inside of the drawer.

fixed
boolean

When true, don't move the drawer upwards if there's space, but rather only change it's height so it's fully scrollable when the keyboard is open

dismissible
true
boolean

When false dragging, clicking outside, pressing esc, etc. will not close the drawer. Use this in combination with the open prop, otherwise you won't be able to open/close the drawer.

modal
true
boolean

When false it allows to interact with elements outside of the drawer without closing it.

open
boolean
defaultOpen
boolean

Opened by default, skips initial enter animation. Still reacts to open state changes

direction
'bottom'
"bottom" | "top" | "left" | "right"

Direction of the drawer. Can be top or bottom, left, right.

noBodyStyles
boolean

When true the body doesn't get any styles assigned from Vaul

handleOnly
boolean

When true only allows the drawer to be dragged by the <Drawer.Handle /> component.

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 [0.2, 0.5, 0.8]. You can also use px values, which doesn't take screen height into account.

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

app.config.ts
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'
          }
        }
      ]
    }
  }
})
vite.config.ts
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'
              }
            }
          ]
        }
      }
    })
  ]
})

Changelog

No recent changes