화면 안팎으로 부드럽게 미끄러지는 서랍

Usage

서랍의 기본 슬롯에 있는 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 슬롯을 사용하여 서랍의 내용을 사용자 정의할 수 있습니다.

Title 파일

title prop을 사용하여 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 prop을 사용하여 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>

Close : badge{label="4.10+" class="align-text-top"}

close prop을 사용하여 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>

Direction

direction Prop을 사용하여 Drawer의 방향을 제어합니다. 기본값은 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 Prop을 사용하여 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>

핸들

handle 소품을 사용하여 Drawer에 핸들이 있는지 여부를 제어합니다. 기본값은 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 전용

handle-only Prop을 사용하여 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 Prop을 사용하여 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 Prop을 사용하여 Drawer 바깥쪽을 클릭하거나 escape 키를 누를 때 Drawer가 표시되지 않도록 제어합니다. 기본값은 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 배경

Drawer가 열려 있을 때 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'
    }
  }
})

예제

Control 오픈 상태

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"}를 눌러 Drawer를 토글할 수 있습니다.
이렇게 하면 트리거를 Drawer 외부로 이동하거나 완전히 제거할 수 있습니다.

응답형 서랍

예를 들어 데스크톱에서는 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 서랍

nested prop을 사용하여 서로 서랍을 중첩 할 수 있습니다.

<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 명령 팔레트

CommandPalette 구성 요소를 Drawer의 콘텐츠 내에서 사용할 수 있습니다.

<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'
              }
            }
          ]
        }
      }
    })
  ]
})

변경 로그

No recent changes