---
title: Drawer
description: A modal panel that slides in from an edge of the screen.
category: Feedback
status: beta
since: 0.3.4
tags: [drawer, panel, overlay]
syncStatus: reviewed
verified: true
---

## Usage

### Basic
With a confirmation dialog open on top, Tab stays inside it and Escape closes only the dialog.
:::TuffDemoWrapper{demo="DrawerBasicDrawerDemo" code-lang="vue"}
---
code: |
  <template>
    <TxButton @click="visible = true">Open drawer</TxButton>
    <TxDrawer v-model:visible="visible" title="Settings">
      <p>This is the drawer content area.</p>
      <TxButton @click="confirmVisible = true">Open confirmation</TxButton>
    </TxDrawer>
    <TxModal v-model="confirmVisible" title="Confirm action">
      <template #footer>
        <TxButton @click="confirmVisible = false">Cancel</TxButton>
      </template>
    </TxModal>
  </template>
---
:::

### Direction
`direction` takes four edges; `size` is the width for left and right, and the height for top and bottom.
:::TuffDemoWrapper{demo="DrawerDirectionDemo" code-lang="vue"}
---
code: |
  <template>
    <TxDrawer v-model:visible="left" title="Left Drawer" direction="left" size="360px" :mobile-adapt="false" />
    <TxDrawer v-model:visible="right" title="Right Drawer" direction="right" size="360px" :mobile-adapt="false" />
    <TxDrawer v-model:visible="top" title="Top Drawer" direction="top" size="18rem" :mobile-adapt="false" />
    <TxDrawer v-model:visible="bottom" title="Bottom Drawer" direction="bottom" size="45%" />
  </template>
---
:::

### Size and Fullscreen
`size` takes a number (px), a CSS length, a percentage, or `'full'`; `full` equals `size="full"`.
:::TuffDemoWrapper{demo="DrawerCustomWidthDemo" code-lang="vue"}
---
code: |
  <template>
    <TxDrawer v-model:visible="px" title="size = 400px" size="400px" :mobile-adapt="false" />
    <TxDrawer v-model:visible="rem" title="size = 24rem" size="24rem" :mobile-adapt="false" />
    <TxDrawer v-model:visible="percent" title="bottom + size = 55%" direction="bottom" size="55%" />
    <TxDrawer v-model:visible="fullscreen" title="Fullscreen drawer" full />
  </template>
---
:::

### Header, Footer, and Mask
The `header` and `footer` slots receive `close`; `maskEffect` sets the mask look, and `panelTransparent` lets the page show through the panel.
:::TuffDemoWrapper{demo="DrawerSlotsEffectsDemo" code-lang="vue"}
---
code: |
  <template>
    <TxDrawer
      v-model:visible="visible"
      title="Release policy"
      size="min(460px, 92vw)"
      mask-effect="opacity"
      panel-transparent
    >
      <template #header="{ close }">
        <strong>Release policy</strong>
        <TxButton variant="ghost" @click="close">Close</TxButton>
      </template>

      <p>Drawer content</p>

      <template #footer="{ close }">
        <TxButton @click="close">Cancel</TxButton>
        <TxButton type="primary" @click="close">Save</TxButton>
      </template>
    </TxDrawer>
  </template>
---
:::

### Form
When `close` isn't needed, the `footer` slot can update outside state directly.

:::TuffCodeBlock{lang="vue"}
---
code: |
  <template>
    <TxDrawer v-model:visible="visible" title="Form">
      <form>
        <input type="text" placeholder="Name" />
      </form>

      <template #footer>
        <TxButton @click="visible = false">Cancel</TxButton>
        <TxButton type="primary" @click="handleSave">Save</TxButton>
      </template>
    </TxDrawer>
  </template>
---
:::

### Close Behavior

:::TuffCodeBlock{lang="vue"}
---
code: |
  <template>
    <!-- Clicking the mask doesn't close it -->
    <TxDrawer v-model:visible="visible" title="Persistent" :close-on-click-mask="false">
      <p>Only the close button closes this drawer</p>
    </TxDrawer>

    <!-- Escape doesn't close it -->
    <TxDrawer v-model:visible="visible2" title="No Escape" :close-on-press-escape="false">
      <p>Escape does not close this drawer</p>
    </TxDrawer>
  </template>
---
:::

### Open and Close Events

:::TuffCodeBlock{lang="vue"}
---
code: |
  <template>
    <TxDrawer v-model:visible="visible" title="Events" @open="handleOpen" @close="handleClose">
      <p>Content</p>
    </TxDrawer>
  </template>
---
:::

### Dashboard Navigation
Tabs hold the top-level sections, DropdownMenu the light actions, Popover the short notes, and Drawer the dense settings.
:::TuffDemoWrapper{demo="ComponentsNavigationShellDemo" code-lang="vue"}
---
code: |
  <template>
    <TxDropdownMenu>
      <template #trigger>
        <TxButton>Release actions</TxButton>
      </template>
      <TxDropdownItem>Quick release</TxDropdownItem>
    </TxDropdownMenu>

    <TxPopover>
      <template #reference>
        <TxButton variant="secondary">Policy notes</TxButton>
      </template>
      Keep popovers to short notes and light actions.
    </TxPopover>

    <TxTabs v-model="active" placement="left" indicator-variant="pill">
      <TxTabItem name="Overview" activation>Overview settings</TxTabItem>
      <TxTabItem name="Release">Release settings</TxTabItem>
    </TxTabs>

    <TxDrawer v-model:visible="drawerVisible" title="Release policy" />
  </template>
---
:::

### Best Practices

- Put long forms, audit details, permission matrices, and flows that need a footer action bar in a drawer, not a popover.
- Give a meaningful `title` even with `showHeader=false`; it is the dialog's accessible name.
- Size the drawer with `size` or `full`; `width` exists only for older call sites.
- Set `mobileAdapt=false` only when a side entrance matters more on small screens than a bottom sheet.
- Let the drawer manage focus instead of moving it from outside; form fields in the body still need explicit labels.

## API Reference

### Props

::TuffPropsTable
---
rows:
  - name: visible
    description: 'Whether the drawer is shown; bind with `v-model:visible`.'
    type: 'boolean'
    default: '*required*'
  - name: title
    description: 'Title; becomes the `aria-label` when no header renders.'
    type: 'string'
    default: "'Drawer'"
  - name: size
    description: 'Size on the active axis: width for left/right, height for top/bottom; numbers are px, `full` is 100%.'
    type: "number | string | 'full'"
    default: "'60%'"
  - name: full
    description: 'Opens at 100% on the active axis; same as `size="full"`.'
    type: 'boolean'
    default: 'false'
  - name: width
    description: 'Legacy alias; use `size`.'
    type: "number | string | 'full'"
    default: '-'
  - name: direction
    description: 'Edge the drawer slides in from.'
    type: "'left' | 'right' | 'top' | 'bottom'"
    default: "'right'"
  - name: showHeader
    description: 'Renders the header area.'
    type: 'boolean'
    default: 'true'
  - name: showFooter
    description: 'Renders the footer slot area.'
    type: 'boolean'
    default: 'true'
  - name: showClose
    description: 'Shows the close button in the default header.'
    type: 'boolean'
    default: 'true'
  - name: closeOnClickMask
    description: 'Closes when the mask is clicked.'
    type: 'boolean'
    default: 'true'
  - name: closeOnPressEscape
    description: 'Closes when Escape is pressed.'
    type: 'boolean'
    default: 'true'
  - name: maskEffect
    description: 'Mask look: blurred, dimmed only, or transparent.'
    type: "'blur' | 'opacity' | 'transparent'"
    default: "'blur'"
  - name: panelTransparent
    description: 'Makes the panel translucent so the page shows through.'
    type: 'boolean'
    default: 'false'
  - name: mobileAdapt
    description: 'Forces a bottom entrance when the viewport is 768px wide or less.'
    type: 'boolean'
    default: 'true'
  - name: zIndex
    description: 'Fixed layer; without it the z-index manager allocates from `10000`.'
    type: 'number'
    default: '-'
  - name: lazy
    description: 'Skips slot content until the first open, then keeps it; set `false` to mount eagerly.'
    type: 'boolean'
    default: 'true'
---
::

### Events

::TuffPropsTable
---
rows:
  - name: update:visible
    description: 'Fires when visibility changes.'
    type: '(visible: boolean) => void'
    default: '-'
  - name: open
    description: 'Fires when `visible` turns `true`.'
    type: '() => void'
    default: '-'
  - name: close
    description: 'Fires when the user closes the drawer; not when the parent sets `visible`.'
    type: '() => void'
    default: '-'
---
::

### Slots

::TuffPropsTable
---
rows:
  - name: default
    description: 'Main content.'
    type: '-'
    default: '-'
  - name: header
    description: 'Replaces the default title and close button; slot props: `{ close, title, titleId }`.'
    type: '-'
    default: '-'
  - name: footer
    description: 'Footer action area; slot props: `{ close }`.'
    type: '-'
    default: '-'
---
::

## Overview

- The root is `role="dialog"` with `aria-modal="true"`; with a header it uses `aria-labelledby`, otherwise `title` becomes the `aria-label`.
- Opening focuses the drawer and Tab cycles inside it; closing or unmounting returns focus to the previously focused element.
- The close button, the mask, and Escape all emit `update:visible(false)` and `close`; `closeOnClickMask` and `closeOnPressEscape` turn off the last two.
- Only the topmost modal dialog handles Tab and Escape, and keys another control already handled are ignored, so a confirmation on top owns them.
- When closed, the root stays in the DOM with `inert` and `aria-hidden`.

## Technologies

- The header and footer dividers are `TxDivider`; don't hard-code borders for them.
- Source: `packages/tuffex/packages/components/src/drawer/`.

<TuffDocSourceLink />
