Skip to content

Drawer ​

Drawer is a panel that slides in from an edge of the viewport. It shares reka-ui's dialog primitive with Dialog, but adds edge positions, start/end variants that mirror under RTL, edge-aware sizes, and configurable dismissal.

vue
<script setup lang="ts">
import { ref } from 'vue'
import { Button, Drawer } from '@myghf/ui'

const open = ref(false)
</script>

<template>
  <Drawer v-model:open="open" position="end" title="Filters">
    <p>Filter controls…</p>
    <template #footer>
      <Button variant="outline" @click="open = false">Close</Button>
    </template>
  </Drawer>
  <Button @click="open = true">Open filters</Button>
</template>

Examples ​

Positions ​

position accepts six values: the physical left / right / top / bottom, plus the logical start and end, which map to those edges and flip under RTL.

vue
<script setup lang="ts">
import { ref } from 'vue'
import { Button, Drawer, type DrawerPosition } from '@myghf/ui'

const open = ref(false)
const position = ref<DrawerPosition>('right')
const positions: DrawerPosition[] = ['left', 'right', 'top', 'bottom', 'start', 'end']

function show(next: DrawerPosition) {
  position.value = next
  open.value = true
}
</script>

<template>
  <ClientOnly>
    <div class="flex flex-wrap gap-2">
      <Button v-for="p in positions" :key="p" variant="outline" size="sm" @click="show(p)">
        {{ p }}
      </Button>
    </div>

    <Drawer
      v-model:open="open"
      :position="position"
      :title="'Position: ' + position"
      description="start/end mirror left/right and flip under RTL."
    >
      <p class="text-sm text-muted">
        The panel slides in from the chosen edge. Vertical positions ignore the horizontal
        size and clamp their height instead.
      </p>
      <template #footer>
        <Button variant="outline" @click="open = false">Close</Button>
      </template>
    </Drawer>

    <template #fallback>
      <span class="text-sm text-muted">Loading drawer…</span>
    </template>
  </ClientOnly>
</template>

Backdrop, dismissal, and scroll ​

Turn off the dimming overlay with backdrop, block Esc with closeOnEscape, block outside clicks with closeOnOutside, and control the body scroll lock with preventScroll.

vue
<script setup lang="ts">
import { ref } from 'vue'
import { Button, Drawer } from '@myghf/ui'

const open = ref(false)
</script>

<template>
  <ClientOnly>
    <Button variant="outline" @click="open = true">Open with custom dismissal</Button>

    <!--
      preventScroll only applies at all when backdrop is false: reka's overlay
      owns the body scroll lock, and this drawer renders no overlay here.
    -->
    <Drawer
      v-model:open="open"
      :backdrop="false"
      :close-on-escape="false"
      :close-on-outside="false"
      :prevent-scroll="false"
      title="Dismissal options"
      description="No dimmer and background scrolling stays enabled; Esc and outside clicks are ignored."
      size="sm"
    >
      <p class="text-sm text-muted">
        <kbd>Esc</kbd> and outside clicks are ignored, so the close button (or your own
        action) is the only way out of this demo.
      </p>
      <template #footer>
        <Button variant="outline" @click="open = false">Close</Button>
      </template>
    </Drawer>

    <template #fallback>
      <span class="text-sm text-muted">Loading drawer…</span>
    </template>
  </ClientOnly>
</template>

preventScroll: false needs backdrop: false

Reka's overlay — not the panel — owns the body scroll lock. The overlay is rendered whenever backdrop or preventScroll is true, so with the default backdrop: true the body stays locked no matter what preventScroll says. To actually allow background scrolling, set both backdrop: false and preventScroll: false.

Props ​

PropTypeDefaultDescription
openbooleanfalseWhether the drawer is open. Bind it with v-model:open.
position'left' | 'right' | 'top' | 'bottom' | 'start' | 'end''right'Edge the panel slides in from. start/end are logical and flip under RTL.
size'sm' | 'md' | 'lg' | 'full''md'Edge-aware size (see below).
backdropbooleantrueRenders the dimming overlay behind the panel.
closeOnEscapebooleantrueWhether Esc dismisses the drawer.
closeOnOutsidebooleantrueWhether a click outside the panel dismisses it.
preventScrollbooleantrueLocks body scroll while the drawer is open. See the caveat above.
titlestring—Header title. Can also be provided through the header slot.
descriptionstring—Supporting line. Can also be provided through the description slot.
showClosebooleantrueRenders the header close button.

Sizes map to the panel's constrained dimension:

SizeHorizontal (left / right / start / end)Vertical (top / bottom)
smmax-w-smmax-h-64
mdmax-w-mdmax-h-96
lgmax-w-lgmax-h-[32rem]
fullmax-w-fullmax-h-full

For top / bottom, the vertical max-height is applied instead of a width.

Events ​

EventPayloadDescription
update:openbooleanEmitted when the drawer requests to open or close. Required for v-model:open.
open—Emitted after the drawer opens.
close—Emitted after the drawer closes.

Slots ​

SlotDescription
triggerElement that opens the drawer, rendered as-child (a single attribute/ref-accepting element).
headerReplaces the title text. Rendered inside the <DialogTitle>.
descriptionReplaces the description text. Rendered inside the <DialogDescription>.
defaultBody content, in the scrollable region.
footerActions row, shown only when the slot is present.
closeReplaces the close button's icon.

Exposed methods ​

None. Drawer does not call defineExpose. Control it through the bound open value.

Accessibility ​

  • Like Dialog, the panel is a reka dialog with role="dialog", aria-modal="true", a focus trap, and focus restored to the trigger on close.
  • If you pass neither title nor the header slot, Drawer renders a visually hidden <DialogTitle> ("Drawer") so the panel always has an accessible name. Prefer a real title that describes the drawer.
  • The close button is a real <button> with aria-label="Close"; use the close slot to change its icon.
  • closeOnEscape: false and closeOnOutside: false cancel reka's default dismissal. Because focus stays trapped inside the panel, a drawer with both disabled and showClose: false would be impossible to dismiss — always leave one exit available.
  • The overlay (backdrop: true) communicates modality visually; when backdrop: false, background content stays visible and the panel is less clearly modal.

Dark mode & RTL ​

  • The overlay uses bg-black/50 dark:bg-black/70; the panel uses semantic tokens (bg-surface, border-border, shadow-dialog), so both themes are covered.
  • start / end are logical (start-0 / end-0) and their slide transforms are mirrored with rtl: variants, so the panel enters from the correct edge in each direction. The physical left / right positions stay put and do not flip.
  • The footer's close-side padding and the header close button are direction-safe; the sm/md/lg widths are symmetric, so no extra RTL overrides are needed.

Released under the MIT License.