Drawer

AkarGitHub
A drawer that smoothly slides in & out of the screen.

Usage

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

Then, use the #content slot to add the content displayed when the Drawer is open.

<template>
  <PDrawer>
    <PButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide:chevron-up" />

    <template #content>
      <CorePlaceholder class="h-48 m-4" />
    </template>
  </PDrawer>
</template>

You can also use the #header, #body and #footer slots to customize the Drawer's content.

Title

Use the title prop to set the title of the Drawer's header.

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

    <template #body>
      <CorePlaceholder class="h-48" />
    </template>
  </PDrawer>
</template>

Description

Use the description prop to set the description of the Drawer's header.

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

    <template #body>
      <CorePlaceholder class="h-48" />
    </template>
  </PDrawer>
</template>

Direction

Use the direction prop to control the direction of the Drawer. Defaults to bottom.

<template>
  <PDrawer direction="right">
    <PButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide:chevron-up" />

    <template #content>
      <CorePlaceholder class="min-w-96 min-h-96 size-full m-4" />
    </template>
  </PDrawer>
</template>

Inset

Use the inset prop to inset the Drawer from the edges.

<template>
  <PDrawer direction="right" inset>
    <PButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide:chevron-up" />

    <template #content>
      <CorePlaceholder class="min-w-96 min-h-96 size-full m-4" />
    </template>
  </PDrawer>
</template>

Handle

Use the handle prop to control whether the Drawer has a handle or not. Defaults to true.

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

    <template #content>
      <CorePlaceholder class="h-48 m-4" />
    </template>
  </PDrawer>
</template>

Handle Only

Use the handle-only prop to only allow the Drawer to be dragged by the handle.

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

    <template #content>
      <CorePlaceholder class="h-48 m-4" />
    </template>
  </PDrawer>
</template>

Overlay

Use the overlay prop to control whether the Drawer has an overlay or not. Defaults to true.

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

    <template #content>
      <CorePlaceholder class="h-48 m-4" />
    </template>
  </PDrawer>
</template>

Use the modal prop to control whether the Drawer blocks interaction with outside content. Defaults to true.

When modal is set to false, the overlay is automatically disabled and outside content becomes interactive.
<template>
  <PDrawer :modal="false">
    <PButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide:chevron-up" />

    <template #content>
      <CorePlaceholder class="h-48 m-4" />
    </template>
  </PDrawer>
</template>

Dismissible

Use the dismissible prop to control whether the Drawer is dismissible when clicking outside of it or pressing escape. Defaults to true.

A close:prevent event will be emitted when the user tries to close it.
You can combine modal: false with dismissible: false to make the Drawer's background interactive without closing it.
<script setup lang="ts">
import { ref } from 'vue'

const open = ref(false)
</script>

<template>
  <PDrawer
    v-model:open="open"
    :dismissible="false"
    :handle="false"
    :pohon="{ header: 'flex items-center justify-between' }"
  >
    <PButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide:chevron-up" />

    <template #header>
      <h2 class="text-highlighted font-semibold">Drawer non-dismissible</h2>

      <PButton color="neutral" variant="ghost" icon="i-lucide:x" @click="open = false" />
    </template>

    <template #body>
      <CorePlaceholder class="h-48" />
    </template>
  </PDrawer>
</template>

Scale Background

Use the should-scale-background prop to scale the background when the Drawer is open, creating a visual depth effect. You can set the set-background-color-on-scale prop to false to prevent changing the background color.

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

    <template #content>
      <CorePlaceholder class="h-48 m-4" />
    </template>
  </PDrawer>
</template>
Make sure to add the data-vaul-drawer-wrapper directive to a parent element of your app to make this work.
app.vue
<template>
  <PApp>
    <div
      class="bg-background"
      data-vaul-drawer-wrapper
    >
      <NuxtLayout>
        <NuxtPage />
      </NuxtLayout>
    </div>
  </PApp>
</template>
nuxt.config.ts
export default defineNuxtConfig({
  app: {
    rootAttrs: {
      'data-vaul-drawer-wrapper': '',
      'class': 'bg-background'
    }
  }
});

Examples

Control open state

You can control the open state by using the default-open prop or the v-model:open directive.

<script setup lang="ts">
import { defineShortcuts } from '#imports'
import { ref } from 'vue'

const open = ref(false)

defineShortcuts({
  o: () => {
    open.value = !open.value
  }
})
</script>

<template>
  <PDrawer v-model:open="open">
    <PButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide:chevron-up" />

    <template #content>
      <CorePlaceholder class="m-4 h-48" />
    </template>
  </PDrawer>
</template>
In this example, leveraging defineShortcuts, you can toggle the Drawer by pressing O.
This allows you to move the trigger outside of the Drawer or remove it entirely.

Responsive drawer

You can render a Dialog component on desktop and a Drawer on mobile for example.

<script lang="ts" setup>
import { createReusableTemplate, useMediaQuery } from '@vueuse/core'
import { reactive, ref } from 'vue'

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>
    <PForm :state="state" class="space-y-4">
      <PFormField label="Email" name="email" required>
        <PInput v-model="state.email" placeholder="shadcn@example.com" required />
      </PFormField>

      <PButton label="Save changes" type="submit" />
    </PForm>
  </DefineFormTemplate>

  <PDialog v-if="isDesktop" v-model:open="open" :title="title" :description="description">
    <PButton label="Edit profile" color="neutral" variant="outline" />

    <template #body>
      <ReuseFormTemplate />
    </template>
  </PDialog>

  <PDrawer v-else v-model:open="open" :title="title" :description="description">
    <PButton label="Edit profile" color="neutral" variant="outline" />

    <template #body>
      <ReuseFormTemplate />
    </template>
  </PDrawer>
</template>

Nested drawers

You can nest drawers within each other by using the nested prop.

<template>
  <PDrawer :pohon="{ content: 'h-full', overlay: 'bg-inverted/30' }">
    <PButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide:chevron-up" />

    <template #footer>
      <PDrawer nested :pohon="{ content: 'h-full', overlay: 'bg-inverted/30' }">
        <PButton color="neutral" variant="outline" label="Open nested" />

        <template #content>
          <CorePlaceholder class="m-4 flex-1" />
        </template>
      </PDrawer>
    </template>
  </PDrawer>
</template>

Use the #footer slot to add content after the Drawer's body.

<script setup lang="ts">
import { ref } from 'vue'

const open = ref(false)
</script>

<template>
  <PDrawer
    v-model:open="open"
    title="Drawer with footer"
    description="This is useful when you want a form in a Drawer."
    :pohon="{ container: 'max-w-xl mx-auto' }"
  >
    <PButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide:chevron-up" />

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

    <template #footer>
      <PButton label="Submit" color="neutral" class="justify-center" />
      <PButton
        label="Cancel"
        color="neutral"
        variant="outline"
        class="justify-center"
        @click="open = false"
      />
    </template>
  </PDrawer>
</template>

With command palette

You can use a CommandPalette component inside the Drawer's content.

<script setup lang="ts">
import { useFetch } from '#app';
import { computed, ref } from 'vue';

const searchTerm = ref('');

const { data: users, status } = await useFetch('https://jsonplaceholder.typicode.com/users', {
  key: 'command-palette-users',
  params: { q: searchTerm },
  transform: (data: Array<{ 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>
  <PDrawer :handle="false">
    <PButton
      label="Search users..."
      color="neutral"
      variant="subtle"
      icon="i-lucide:search"
    />

    <template #content>
      <PCommandPalette
        v-model:search-term="searchTerm"
        :loading="status === 'pending'"
        :groups="groups"
        placeholder="Search users..."
        class="h-80"
      />
    </template>
  </PDrawer>
</template>

API

Props

Prop Default Type
as'div'any

The element or component this component should render as.

portaltruestring | false | true | HTMLElement

Render the drawer in a portal.

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

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

titlestring
descriptionstring
contentADialogContentProps & Partial<EmitsToProps<DialogContentImplEmits>>

The content of the drawer.

activeSnapPointstring | number
closeThresholdnumber

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.

scrollLockTimeoutnumber

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

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.

overlaytrueboolean

Render an overlay behind the drawer.

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.

modaltrueboolean

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

handletrueboolean

Render a handle on the drawer.

openboolean
defaultOpenboolean

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

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

shouldScaleBackgroundboolean
setBackgroundColorOnScaleboolean

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

nestedfalseboolean

Whether the drawer is nested in another drawer.

noBodyStylesboolean

When true the body doesn't get any styles assigned.

handleOnlyboolean

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

preventScrollRestorationboolean
insetfalseboolean

Whether to inset the drawer from the edges.

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

Slots

Slot Type
defaultobject
contentobject
headerobject
titleobject
descriptionobject
bodyobject
footerobject

Emits

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

Theme

We use unocss-variants to customize the theme. Read more about it in the theming guide.

Below is the theme configuration skeleton for the PDrawer. Since the component is provided unstyled by default, you will need to fill in these values to apply your own custom look and feel. If you prefer to use our pre-built, opinionated styling, you can instead use our UnoCSS preset, this docs is using it as well.

app.config.ts
export default defineAppConfig({
  pohon: {
    drawer: {
      slots: {
        overlay: '',
        content: '',
        handle: '',
        container: '',
        header: '',
        title: '',
        description: '',
        body: '',
        footer: ''
      },
      variants: {
        direction: {
          top: {
            content: '',
            handle: ''
          },
          right: {
            content: '',
            handle: ''
          },
          bottom: {
            content: '',
            handle: ''
          },
          left: {
            content: '',
            handle: ''
          }
        },
        inset: {
          true: {
            content: ''
          }
        },
        snapPoints: {
          true: ''
        }
      },
      compoundVariants: []
    }
  }
};
vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import pohon from 'pohon-ui/vite'

export default defineAppConfig({
  pohon: {
    drawer: {
      slots: {
        overlay: '',
        content: '',
        handle: '',
        container: '',
        header: '',
        title: '',
        description: '',
        body: '',
        footer: ''
      },
      variants: {
        direction: {
          top: {
            content: '',
            handle: ''
          },
          right: {
            content: '',
            handle: ''
          },
          bottom: {
            content: '',
            handle: ''
          },
          left: {
            content: '',
            handle: ''
          }
        },
        inset: {
          true: {
            content: ''
          }
        },
        snapPoints: {
          true: ''
        }
      },
      compoundVariants: []
    }
  }
};

Changelog

No recent changes