---
title: "Drawer"
description: "A drawer that smoothly slides in and out of the screen."
canonical_url: "https://vinicunca.dev/pohon/components/drawer"
---
# Drawer

> A drawer that smoothly slides in and out of the screen.

## Usage

Use a [Button](https://vinicunca.dev/pohon/components/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.

```vue
<template>
  <PDrawer>
    <PButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide-chevron-up" />
  
    <template #content>
      <Placeholder 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.

```vue
<template>
  <PDrawer title="Drawer with title">
    <PButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide-chevron-up" />
  
    <template #body>
      <Placeholder class="h-48" />
    </template></PDrawer>
</template>
```

### Description

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

```vue
<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>
      <Placeholder class="h-48" />
    </template></PDrawer>
</template>
```

### Close

Use the `close` prop to display a close button in the Drawer. Defaults to `false`.

You can pass any property from the [Button](https://vinicunca.dev/pohon/components/button) component to customize it.

```vue
<template>
  <PDrawer title="Drawer with close button" :close="{
  color: 'primary',
  variant: 'outline',
  class: 'rounded-full'
}">
    <PButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide-chevron-up" />
  
    <template #body>
      <Placeholder class="h-48" />
    </template></PDrawer>
</template>
```

### Close Icon

Use the `close-icon` prop to customize the close button [Icon](https://vinicunca.dev/pohon/components/icon). Defaults to `i-lucide-x`.

```vue
<template>
  <PDrawer title="Drawer with close button" close close-icon="i-lucide-arrow-right">
    <PButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide-chevron-up" />
  
    <template #body>
      <Placeholder class="h-48" />
    </template></PDrawer>
</template>
```

### Direction

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

```vue
<template>
  <PDrawer direction="right">
    <PButton 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></PDrawer>
</template>
```

### Inset

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

```vue
<template>
  <PDrawer direction="right" inset>
    <PButton 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></PDrawer>
</template>
```

### Handle

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

```vue
<template>
  <PDrawer :handle="false">
    <PButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide-chevron-up" />
  
    <template #content>
      <Placeholder 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`.

```vue
<template>
  <PDrawer :overlay="false">
    <PButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide-chevron-up" />
  
    <template #content>
      <Placeholder class="h-48 m-4" />
    </template></PDrawer>
</template>
```

### Modal

Use the `modal` prop to control whether the Drawer blocks interaction with outside content. Defaults to `true`. Set it to `'trap-focus'` to trap keyboard focus while allowing outside pointer interaction, without a backdrop or body scroll lock.

> [!NOTE]
> 
> When `modal` is set to `false`, the overlay is automatically disabled and outside content becomes interactive.

```vue
<template>
  <PDrawer :modal="false">
    <PButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide-chevron-up" />
  
    <template #content>
      <Placeholder class="h-48 m-4" />
    </template></PDrawer>
</template>
```

### Dismissible

Use the `dismissible` prop to control whether the Drawer is dismissible when swiping, clicking outside of it, or pressing Escape. Defaults to `true`.

> [!NOTE]
> 
> A `close:prevent` event will be emitted when the user tries to close it.

> [!TIP]
> 
> You can combine `modal: false` with `dismissible: false` to make the Drawer's background interactive without closing it.

```vue [DrawerDismissibleExample.vue]
<script setup lang="ts">
const open = ref(false);
</script>

<template>
  <PDrawer
    v-model:open="open"
    title="Drawer non-dismissible"
    close
    :dismissible="false"
    :modal="false"
    :handle="false"
  >
    <PButton
      label="Open"
      color="neutral"
      variant="subtle"
      trailing-icon="i-lucide-chevron-up"
    />

    <template #body>
      <Placeholder class="size-full min-h-48" />
    </template>
  </PDrawer>
</template>
```

### Scale Background `Soon`

Wrap page content in [DrawerProvider](https://vinicunca.dev/pohon/components/drawer-provider) with `scale` to scale it whenever a descendant Drawer is open. Leave the Drawer's portal enabled so the drawer renders outside the scaled content.

```vue [app.vue]
<template>
  <PApp>
    <PDrawerProvider
      scale
      class="min-h-dvh"
      :ui="{ content: 'min-h-dvh' }"
    >
      <NuxtLayout>
        <NuxtPage />
      </NuxtLayout>
    </PDrawerProvider>
  </PApp>
</template>
```

### Snap Points `Soon`

Use `snap-points` to define snap positions, `default-snap-point` for the initial uncontrolled position, or `v-model:snap-point` to control it. Enable `snap-to-sequential-points` to advance one position at a time.

```vue
<PDrawer :snap-points="[0.5, 1]" :default-snap-point="0.5" snap-to-sequential-points title="Snap points">
  <PButton label="Open" />
  <template #body>
    <Placeholder class="h-screen w-full" />
  </template>
</PDrawer>
```

Numeric fractions use the viewport height; numbers greater than 1 are pixels. Strings support `px` and `rem`.

### Migrating from Vaul `Soon`

Drawer now uses Akar. Replace `activeSnapPoint` and its update event with `snapPoint` / `update:snapPoint`. Nesting is automatic; remove `nested`. Replace the Vaul wrapper attribute and scaling props with `PDrawerProvider scale`.

The Vaul-only props `handleOnly`, `closeThreshold`, `scrollLockTimeout`, `fixed`, `noBodyStyles`, and `preventScrollRestoration` have been removed. The handle is now a visual swipe affordance; it no longer cycles snap points on click. Use `after:enter`, `after:leave`, or `update:openComplete` instead of `animationEnd`; `drag`, `release`, and `close` events are no longer exposed.

`update:open` receives `(open, details)`, where `details.reason` describes the interaction and `details.cancel()` rejects it. `content` now uses Akar's drawer-specific types, including `initialFocus` and `finalFocus`.

## Examples

### Control open state

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

```vue [DrawerOpenExample.vue]
<script setup lang="ts">
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>
      <Placeholder class="m-4 h-48" />
    </template>
  </PDrawer>
</template>
```

> [!NOTE]
> 
> In this example, leveraging [`defineShortcuts`](https://vinicunca.dev/pohon/composables/define-shortcuts), you can toggle the Drawer by pressing `O`.

> [!TIP]
> 
> This lets you move the trigger outside of the Drawer or remove it entirely.

### Responsive drawer

You can render a [Modal](https://vinicunca.dev/pohon/components/modal) component on desktop and a Drawer on mobile for example.

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

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

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

    <template #body>
      <ReuseFormTemplate />
    </template>
  </PModal>

  <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

Drawers nested inside another Drawer’s content are detected automatically; no `nested` prop is needed.

```vue [DrawerNestedExample.vue]
<template>
  <PDrawer :ui="{ content: 'h-full', overlay: 'bg-background-inverted/30' }">
    <PButton
      label="Open"
      color="neutral"
      variant="subtle"
      trailing-icon="i-lucide-chevron-up"
    />

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

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

### With footer slot

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

```vue [DrawerFooterSlotExample.vue]
<script setup lang="ts">
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."
    :ui="{ container: 'max-w-xl mx-auto' }"
  >
    <PButton
      label="Open"
      color="neutral"
      variant="subtle"
      trailing-icon="i-lucide-chevron-up"
    />

    <template #body>
      <Placeholder 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](https://vinicunca.dev/pohon/components/command-palette) component inside the Drawer's content.

```vue [DrawerCommandPaletteExample.vue]
<script setup lang="ts">
const searchTerm = ref('');

const { data: users, status, execute } = await useLazyFetch('https://jsonplaceholder.typicode.com/users', {
  key: 'drawer-command-palette-users',
  query: { 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}`, 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>
  <PDrawer
    :handle="false"
    @update:open="onOpen"
  >
    <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>
```

> [!NOTE]
> 
> This example uses `useLazyFetch` with `immediate: false` to only fetch data when the Drawer opens.

## API

### Props

```ts
/**
 * Props for the Drawer component
 */
interface DrawerProps {
  /**
   * The element or component this component should render as.
   * @default 'div'
   */
  as?: any;
  /**
   * The edge from which the drawer opens.
   * @default 'bottom'
   */
  direction?: "top" | "right" | "bottom" | "left" | undefined;
  /**
   * Allow dismissal by swiping, pressing Escape, or interacting outside.
   * @default true
   */
  dismissible?: boolean | undefined;
  /**
   * Animate the drawer and overlay.
   * @default true
   */
  transition?: boolean | undefined;
  title?: string | undefined;
  description?: string | undefined;
  /**
   * Whether to inset the drawer from the edges.
   * @default false
   */
  inset?: boolean | undefined;
  /**
   * The content of the drawer.
   */
  content?: Omit<DrawerContentProps, "as" | "asChild" | "forceMount"> & Partial<EmitsToProps<DrawerContentImplEmits>> | undefined;
  /**
   * Render an overlay behind the drawer.
   * @default true
   */
  overlay?: boolean | undefined;
  /**
   * Render a handle on the drawer.
   * @default true
   */
  handle?: boolean | undefined;
  /**
   * Render the drawer in a portal.
   * @default true
   */
  portal?: string | false | true | HTMLElement | undefined;
  /**
   * Display a close button to dismiss the drawer.
   * `{ size: 'md', color: 'neutral', variant: 'ghost' }`{lang="ts-type"}
   * @default false
   */
  close?: boolean | Omit<ButtonProps, LinkPropsKeys> | undefined;
  /**
   * The icon displayed in the close button.
   * @default appConfig.ui.icons.close
   */
  closeIcon?: any;
  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; } | undefined;
  /**
   * v-model:open
   */
  open?: boolean | undefined;
  defaultOpen?: boolean | undefined;
  /**
   * Modality of the drawer.
   * - `true` (default): modal with focus trap, scroll lock, and outside-press dismiss
   * - `'trap-focus'`: traps focus but allows outside pointer events (non-modal side panels)
   * - `false`: non-modal
   */
  modal?: false | true | "trap-focus" | undefined;
  /**
   * Preset snap positions (fractions 0-1, pixels >1, or '148px'/'30rem' strings)
   */
  snapPoints?: DrawerSnapPoint[] | undefined;
  /**
   * v-model:snapPoint
   */
  snapPoint?: null | string | number | undefined;
  defaultSnapPoint?: null | string | number | undefined;
  /**
   * When true, snaps to the next sequential snap point (one step at a time).
   * When false, snaps to the nearest snap point by distance.
   */
  snapToSequentialPoints?: boolean | undefined;
}
```

### Slots

```ts
/**
 * Slots for the Drawer component
 */
interface DrawerSlots {
  default(): any;
  content(): any;
  header(): any;
  title(): any;
  description(): any;
  actions(): any;
  close(): any;
  body(): any;
  footer(): any;
}
```

### Emits

```ts
/**
 * Emitted events for the Drawer component
 */
interface DrawerEmits {
  after:leave: (payload: []) => void;
  after:enter: (payload: []) => void;
  close:prevent: (payload: []) => void;
  update:open: (payload: [value: boolean, details: DrawerOpenChangeDetails]) => void;
  update:openComplete: (payload: [value: boolean]) => void;
  update:snapPoint: (payload: [value: string | number | null]) => void;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  ui: {
    drawer: {
      slots: {
        overlay: 'pohon-drawer-overlay fixed inset-0 bg-background-elevated/75',
        content: 'pohon-drawer-content fixed bg-background ring ring-border flex min-h-0 focus:outline-none',
        handle: [
          'bg-background-accented rounded-full shrink-0',
          'transition-opacity ease-out'
        ],
        container: 'w-full min-h-0 flex flex-col gap-4 p-4 overflow-y-auto overscroll-contain',
        header: 'flex items-center gap-1.5 min-h-8',
        wrapper: 'min-w-0 flex-1',
        title: 'color-text-highlighted font-600',
        description: 'mt-1 color-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'
          }
        },
        transition: {
          true: {
            content: 'pohon-drawer-transition',
            overlay: 'pohon-drawer-transition'
          }
        },
        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 my-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

See commit history for [component](https://github.com/vinicunca/pohon/commits/main/src/runtime/components/Drawer.vue) and [theme](https://github.com/vinicunca/pohon/commits/main/src/theme/drawer.ts).

---

- [Drawer](https://vinicunca.dev/akar/components/drawer)
- [GitHub](https://github.com/vinicunca/pohon/blob/main/src/runtime/components/Drawer.vue)


## Sitemap

See the full [sitemap](https://vinicunca.dev/sitemap.md) for all pages.
