Drawer

DrawerGitLab
Uma gaveta que desliza suavemente para dentro e para fora da tela.

Uso

Use a Button or any other component in the default slot of the Drawer.

Depois, use o slot #content para adicionar o conteúdo exibido quando o Drawer está aberto.

<template>
  <NDrawer>
    <NButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide-chevron-up" />

    <template #content>
      <Placeholder class="h-48 m-4" />
    </template>
  </NDrawer>
</template>

Você também pode usar os slots #header, #body e #footer para personalizar o conteúdo do Drawer.

Título

Use a prop title para definir o título do cabeçalho do Drawer.

<template>
  <NDrawer title="Drawer with title">
    <NButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide-chevron-up" />

    <template #body>
      <Placeholder class="h-48" />
    </template>
  </NDrawer>
</template>

Descrição

Use a prop description para definir a descrição do cabeçalho do Drawer.

<template>
  <NDrawer
    title="Drawer with description"
    description="Lorem ipsum dolor sit amet, consectetur adipiscing elit."
  >
    <NButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide-chevron-up" />

    <template #body>
      <Placeholder class="h-48" />
    </template>
  </NDrawer>
</template>

Fechar Soon

Use a prop close para exibir um botão de fechar no Drawer. O padrão é false.

Você pode passar qualquer propriedade do componente Button para personalizá-lo.

<template>
  <NDrawer
    title="Drawer with close button"
    :close="{
      color: 'primary',
      variant: 'outline',
      class: 'rounded-full'
    }"
  >
    <NButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide-chevron-up" />

    <template #body>
      <Placeholder class="h-48" />
    </template>
  </NDrawer>
</template>

Ícone de fechar Soon

Use a prop close-icon para personalizar o Icon do botão de fechar. O padrão é i-lucide-x.

<template>
  <NDrawer title="Drawer with close button" close close-icon="i-lucide-arrow-right">
    <NButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide-chevron-up" />

    <template #body>
      <Placeholder class="h-48" />
    </template>
  </NDrawer>
</template>

Direção

Use a prop direction para controlar a direção do Drawer. O padrão é bottom.

<template>
  <NDrawer direction="right">
    <NButton 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>
  </NDrawer>
</template>

Inset

Use a prop inset para recuar o Drawer em relação às bordas.

<template>
  <NDrawer direction="right" inset>
    <NButton 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>
  </NDrawer>
</template>

Alça

Use a prop handle para controlar se o Drawer tem uma alça ou não. O padrão é true.

<template>
  <NDrawer :handle="false">
    <NButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide-chevron-up" />

    <template #content>
      <Placeholder class="h-48 m-4" />
    </template>
  </NDrawer>
</template>

Somente a alça

Use a prop handle-only para permitir que o Drawer seja arrastado apenas pela alça.

<template>
  <NDrawer handle-only>
    <NButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide-chevron-up" />

    <template #content>
      <Placeholder class="h-48 m-4" />
    </template>
  </NDrawer>
</template>

Overlay

Use a prop overlay para controlar se o Drawer tem um overlay ou não. O padrão é true.

<template>
  <NDrawer :overlay="false">
    <NButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide-chevron-up" />

    <template #content>
      <Placeholder class="h-48 m-4" />
    </template>
  </NDrawer>
</template>

Use a prop modal para controlar se o Drawer bloqueia a interação com o conteúdo externo. O padrão é true.

Quando modal é definido como false, o overlay é automaticamente desabilitado e o conteúdo externo se torna interativo.
<template>
  <NDrawer :modal="false">
    <NButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide-chevron-up" />

    <template #content>
      <Placeholder class="h-48 m-4" />
    </template>
  </NDrawer>
</template>

Dispensável

Use a prop dismissible para controlar se o Drawer pode ser dispensado ao clicar fora dele ou pressionar escape. O padrão é true.

Um evento close:prevent será emitido quando o usuário tentar fechá-lo.
Você pode combinar modal: false com dismissible: false para tornar o fundo do Drawer interativo sem fechá-lo.

Escalar o fundo

Use a prop should-scale-background para escalar o fundo quando o Drawer está aberto, criando um efeito visual de profundidade. Você pode definir a prop set-background-color-on-scale como false para evitar a mudança da cor de fundo.

<template>
  <NDrawer should-scale-background set-background-color-on-scale>
    <NButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide-chevron-up" />

    <template #content>
      <Placeholder class="h-48 m-4" />
    </template>
  </NDrawer>
</template>
Certifique-se de adicionar a diretiva data-vaul-drawer-wrapper a um elemento pai do seu app para que isso funcione.
app.vue
<template>
  <NApp>
    <div class="bg-default" data-vaul-drawer-wrapper>
      <NuxtLayout>
        <NuxtPage />
      </NuxtLayout>
    </div>
  </NApp>
</template>
nuxt.config.ts
export default defineNuxtConfig({
  app: {
    rootAttrs: {
      'data-vaul-drawer-wrapper': '',
      'class': 'bg-default'
    }
  }
})

Exemplos

Controlar o estado de abertura

Você pode controlar o estado de abertura usando a prop default-open ou a diretiva v-model:open.

Neste exemplo, aproveitando o defineShortcuts, você pode abrir e fechar o Drawer pressionando O.
Isso permite mover o gatilho para fora do Drawer ou removê-lo por completo.

Gaveta responsiva

Você pode renderizar um componente Modal no desktop e um Drawer no mobile, por exemplo.

Gavetas aninhadas

Você pode aninhar gavetas umas dentro das outras usando a prop nested.

Com slot de rodapé

Use o slot #footer para adicionar conteúdo após o corpo do Drawer.

Com paleta de comandos

Você pode usar um componente CommandPalette dentro do conteúdo do Drawer.

Este exemplo usa useLazyFetch com immediate: false para buscar os dados apenas quando o Drawer abre.

API

Props

Prop Default Type
as'div'any

The element or component this component should render as.

title string
description string
insetfalseboolean

Whether to inset the drawer from the edges.

content DialogContentProps & Partial<EmitsToProps<DialogContentImplEmits>>

The content of the drawer.

overlaytrueboolean

Render an overlay behind the drawer.

handletrueboolean

Render a handle on the drawer.

portaltrue string | false | true | HTMLElement

Render the drawer in a portal.

nestedfalseboolean

Whether the drawer is nested in another drawer.

modaltrueboolean

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

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

shouldScaleBackgroundboolean
setBackgroundColorOnScaleboolean

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.

fixedboolean

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

dismissibletrueboolean

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.

defaultOpenboolean

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

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

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

noBodyStylesboolean

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

handleOnlyboolean

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

preventScrollRestorationboolean
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; title?: SlotClass; description?: SlotClass; body?: SlotClass; footer?: SlotClass; }

Slots

Slot Type
default{}
content{}
header{}
title{}
description{}
body{}
footer{}

Emits

Event Type
update:open[open: boolean]
drag[percentageDragged: number]
close[]
close:prevent[]
release[open: boolean]
update:activeSnapPoint[val: string | number]
animationEnd[open: boolean]

Tema

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 [--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 '@nitro/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 [--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