Drawer

DrawerGitHub
一个平滑地滑入和滑出屏幕的抽屉。

用法

在抽屉组件的默认插槽中,使用一个 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 属性设置抽屉标题。

<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 属性设置抽屉的头部描述。

<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 属性控制抽屉的方向。默认为 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 属性使抽屉从边缘内嵌。

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

缩放背景

使用 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 指令添加到您应用程序的父元素上,以便其正常工作。
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'
    }
  }
})

示例

控制打开状态

您可以通过使用 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>
在此示例中,利用 defineShortcuts,您可以通过按下 O 键来切换抽屉。
这允许您将触发器移到抽屉外部或完全移除它。

禁用关闭

dismissible 属性设置为 false,以防止在点击抽屉外部或按下 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 插槽用于添加一个关闭按钮,这并非默认行为。

可交互背景

overlaymodal 属性以及 dismissible 属性设置为 false,使抽屉背景可交互而不关闭抽屉。

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

响应式抽屉

例如,您可以在桌面端渲染一个 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="[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>

嵌套抽屉 New

您可以通过使用 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 插槽在抽屉主体内容之后添加内容。

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

带命令面板

您可以在抽屉内容中使用 CommandPalette 组件。

<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

属性

属性默认值类型
as

'div'

any

此组件应渲染为的元素或组件。

标题

string

描述

string

内嵌

false

boolean

是否使抽屉从边缘内嵌。

内容

DialogContentProps & Partial<EmitsToProps<DialogContentImplEmits>>

抽屉的内容。

叠加层

true

boolean

在抽屉后面渲染一个遮罩层。

拖动手柄

true

boolean

在抽屉上渲染一个拖动手柄。

portal

true

string | false | true | HTMLElement

在 portal 中渲染抽屉。

nested

false

boolean

抽屉是否嵌套在另一个抽屉中。

fixed

boolean

当为 true 时,如果存在空间,则不向上移动抽屉,而是只改变其高度,以便在键盘打开时可以完全滚动。

open

boolean

defaultOpen

boolean

默认打开,跳过初始进入动画。仍响应 open 状态变化

activeSnapPoint

null | string | number

closeThreshold

number

一个介于 0 和 1 之间的数字,用于确定何时关闭抽屉。示例:阈值为 0.5 将在用户滑动抽屉高度的 50% 或更多时关闭抽屉。

shouldScaleBackground

boolean

setBackgroundColorOnScale

boolean

false 时,在抽屉打开时我们不会改变 body 的背景颜色。

scrollLockTimeout

number

在抽屉内滚动内容后,抽屉不可拖动的时间持续长度。

可关闭的

true

boolean

当为 false 时,拖动、点击外部、按下 Escape 键等操作将不会关闭抽屉。请将其与 open 属性结合使用,否则您将无法打开/关闭抽屉。

modal

true

boolean

当为 false 时,允许与抽屉外部的元素交互而不关闭抽屉。

方向

'bottom'

"top" | "right" | "bottom" | "left"

抽屉的方向。可以是 topbottomleftright

noBodyStyles

boolean

当为 true 时,body 不会从 Vaul 获得任何样式。

handleOnly

boolean

当为 true 时,只允许通过 <Drawer.Handle /> 组件拖动抽屉。

preventScrollRestoration

boolean

snapPoints

(string | number)[]

0 到 100 之间的数字数组,对应于给定吸附点应占屏幕的百分比。应从最不显眼的值开始。例如 [0.2, 0.5, 0.8]。您也可以使用像素值,这不考虑屏幕高度。

ui

{ overlay?: ClassNameValue; content?: ClassNameValue; handle?: ClassNameValue; container?: ClassNameValue; header?: ClassNameValue; title?: ClassNameValue; description?: ClassNameValue; body?: ClassNameValue; footer?: ClassNameValue; }

插槽

插槽类型
默认

{}

内容

{}

页头

{}

标题

{}

描述

{}

主体

{}

页脚

{}

事件

事件类型
关闭

[]

拖动

[percentageDragged: number]

释放

[open: boolean]

update:open

[open: boolean]

update:activeSnapPoint

[val: string | number]

动画结束

[open: boolean]

主题

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'
        ],
        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'
          }
        }
      ]
    }
  }
})
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'
            ],
            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'
              }
            }
          ]
        }
      }
    })
  ]
})
vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import uiPro from '@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'
              }
            }
          ]
        }
      }
    })
  ]
})