Drawer

Slide-out panel for supplementary content and actions

Usage

import { Drawer } from '@hareui/vue'
<script setup lang="ts">import { Button, Drawer } from '@hareui/vue'</script><template>  <Drawer :close-button="false" placement="right" title="Drawer Title">    <Button variant="secondary">      Open Drawer    </Button>    <template #body>      <p>        This is a bottom drawer built with Reka UI's Dialog component. It slides up from        the bottom of the screen with a smooth CSS transition.      </p>    </template>    <template #footer="{ close }">      <Button variant="secondary" @click="close">        Cancel      </Button>      <Button @click="close">        Confirm      </Button>    </template>  </Drawer></template>

Anatomy

A single Drawer component owns the whole structure. The first child is the trigger (as-child, merged directly onto its own root element); everything else is supplied through props and named slots.

<Drawer handle placement="bottom" title="Heading">
  <Button>Open Drawer</Button>
  <template #body>Body content</template>
  <template #footer="{ close }">
    <Button variant="secondary" @click="close">Cancel</Button>
    <Button @click="close">Confirm</Button>
  </template>
</Drawer>

handle adds the optional drag handle, closeButton (on by default) the close button. ui keys: backdrop, content, dialog, handle, header, heading, body, footer, closeTrigger.

Examples

Placement

<script setup lang="ts">import { Button, Drawer } from '@hareui/vue'const placements = ['bottom', 'top', 'left', 'right'] as constconst capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1)</script><template>  <div class="flex flex-wrap gap-4">    <Drawer      v-for="placement in placements"      :key="placement"      :handle="placement === 'bottom' || placement === 'top'"      :placement="placement"      :title="`${capitalize(placement)} Drawer`"    >      <Button variant="secondary">        {{ capitalize(placement) }}      </Button>      <template #body>        <p>          This drawer slides in from the <strong>{{ placement }}</strong> edge of the screen.        </p>      </template>      <template #footer="{ close }">        <Button variant="secondary" @click="close">          Cancel        </Button>        <Button @click="close">          Done        </Button>      </template>    </Drawer>  </div></template>

Non-Dismissable

Set :dismissible="false" to prevent closing by clicking outside or dragging. The user must interact with the drawer's action buttons.

<script setup lang="ts">import { Button, Drawer } from '@hareui/vue'</script><template>  <Drawer :close-button="false" :dismissible="false" title="Confirm Action">    <Button variant="secondary">      Important Action    </Button>    <template #body>      <p>        This drawer cannot be dismissed by clicking outside or dragging. You must use one of        the buttons below.      </p>    </template>    <template #footer="{ close }">      <Button variant="secondary" @click="close">        Cancel      </Button>      <Button @click="close">        Confirm      </Button>    </template>  </Drawer></template>

Scrollable Content

The #body area automatically handles overflow with native scrolling. Drag-to-dismiss is excluded from the body area to avoid scroll conflicts.

<script setup lang="ts">import { Button, Drawer } from '@hareui/vue'</script><template>  <Drawer handle title="Terms & Conditions">    <Button variant="secondary">      Terms & Conditions    </Button>    <template #body>      <p v-for="i in 20" :key="i" class="mb-3">        Paragraph {{ i }}: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam        pulvinar risus non risus hendrerit venenatis. Pellentesque sit amet hendrerit        risus, sed porttitor quam.      </p>    </template>    <template #footer="{ close }">      <Button variant="secondary" @click="close">        Decline      </Button>      <Button @click="close">        Accept      </Button>    </template>  </Drawer></template>

Controlled State

With v-model:open

Control the drawer using Vue's ref and v-model:open for simple state management.

Status: closed

With useOverlayState()

Use the useOverlayState composable for a cleaner API with methods like open(), close(), and toggle().

Status: closed

<script setup lang="ts">import { Button, Drawer } from '@hareui/vue'import { reactive, ref } from 'vue'const isOpen = ref(false)// Vue counterpart of HeroUI's `useOverlayState` hook: a ref plus named open/close/toggle helpers.function useOverlayState(defaultOpen = false) {  const isOpen = ref(defaultOpen)  return reactive({    isOpen,    open: () => { isOpen.value = true },    close: () => { isOpen.value = false },    toggle: () => { isOpen.value = !isOpen.value },    setOpen: (value: boolean) => { isOpen.value = value },  })}const state = useOverlayState()</script><template>  <div class="flex max-w-md flex-col gap-8">    <div class="flex flex-col gap-3">      <h3 class="text-lg font-semibold text-foreground">        With v-model:open      </h3>      <p class="text-sm leading-relaxed text-pretty text-muted">        Control the drawer using Vue's <code class="text-foreground">ref</code> and        <code class="text-foreground">v-model:open</code> for simple state management.      </p>      <div class="flex flex-col items-start gap-3 rounded-2xl bg-surface p-4 shadow-sm">        <div class="flex w-full items-center justify-between">          <p class="text-xs text-muted">            Status:            <span class="font-mono font-medium text-foreground">{{ isOpen ? 'open' : 'closed' }}</span>          </p>        </div>        <div class="flex gap-2">          <Button size="sm" variant="secondary" @click="isOpen = true">            Open Drawer          </Button>          <Button size="sm" variant="tertiary" @click="isOpen = !isOpen">            Toggle          </Button>        </div>      </div>      <Drawer v-model:open="isOpen" placement="right" title="Controlled with v-model:open">        <template #body>          <p>            This drawer is controlled by Vue's <code>ref</code>. Bind            <code>v-model:open</code> to manage the drawer state externally.          </p>        </template>        <template #footer="{ close }">          <Button variant="secondary" @click="close">            Close          </Button>        </template>      </Drawer>    </div>    <div class="flex flex-col gap-3">      <h3 class="text-lg font-semibold text-foreground">        With useOverlayState()      </h3>      <p class="text-sm leading-relaxed text-pretty text-muted">        Use the <code class="text-foreground">useOverlayState</code> composable for a cleaner API        with methods like <code>open()</code>, <code>close()</code>, and <code>toggle()</code>.      </p>      <div class="flex flex-col items-start gap-3 rounded-2xl bg-surface p-4 shadow-sm">        <div class="flex w-full items-center justify-between">          <p class="text-xs text-muted">            Status:            <span class="font-mono font-medium text-foreground">{{ state.isOpen ? 'open' : 'closed' }}</span>          </p>        </div>        <div class="flex gap-2">          <Button size="sm" variant="secondary" @click="state.open">            Open Drawer          </Button>          <Button size="sm" variant="tertiary" @click="state.toggle">            Toggle          </Button>        </div>      </div>      <Drawer :open="state.isOpen" @update:open="state.setOpen" placement="right" title="Controlled with useOverlayState()">        <template #body>          <p>            The <code>useOverlayState</code> composable provides dedicated methods for common            operations. No need to manually create callbacks—just use            <code>state.open()</code>, <code>state.close()</code>, or            <code>state.toggle()</code>.          </p>        </template>        <template #footer="{ close }">          <Button variant="secondary" @click="close">            Close          </Button>        </template>      </Drawer>    </div>  </div></template>

With Form

<script setup lang="ts">import { Button, Drawer, TextField } from '@hareui/vue'</script><template>  <Drawer placement="right" title="Edit Profile">    <Button variant="secondary">      Edit Profile    </Button>    <template #body>      <form class="flex flex-col gap-4">        <TextField class="w-full" label="Name" name="name" placeholder="Enter your name" type="text" variant="secondary" />        <TextField class="w-full" label="Email" name="email" placeholder="Enter your email" type="email" variant="secondary" />        <TextField class="w-full" label="Bio" name="bio" placeholder="Tell us about yourself" variant="secondary" />      </form>    </template>    <template #footer="{ close }">      <Button variant="secondary" @click="close">        Cancel      </Button>      <Button @click="close">        Save Changes      </Button>    </template>  </Drawer></template>
<script setup lang="ts">import { Button, Drawer } from '@hareui/vue'import { Icon } from '@iconify/vue'const navItems = [  { icon: 'gravity-ui:house', label: 'Home' },  { icon: 'gravity-ui:magnifier', label: 'Search' },  { icon: 'gravity-ui:bell', label: 'Notifications' },  { icon: 'gravity-ui:envelope', label: 'Messages' },  { icon: 'gravity-ui:person', label: 'Profile' },  { icon: 'gravity-ui:gear', label: 'Settings' },]</script><template>  <Drawer placement="left" title="Navigation">    <Button variant="secondary">      <Icon height="16" icon="gravity-ui:bars" width="16" />      Menu    </Button>    <template #body>      <nav class="flex flex-col gap-1">        <button          v-for="item in navItems"          :key="item.label"          class="flex items-center gap-3 rounded-xl px-3 py-2.5 text-sm text-foreground transition-colors hover:bg-default"          type="button"        >          <Icon class="size-5 text-muted" :icon="item.icon" />          {{ item.label }}        </button>      </nav>    </template>  </Drawer></template>

Backdrop Variants

<script setup lang="ts">import { Button, Drawer } from '@hareui/vue'const variants = ['opaque', 'blur', 'transparent'] as constconst capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1)</script><template>  <div class="flex flex-wrap gap-4">    <Drawer      v-for="variant in variants"      :key="variant"      handle      :title="`Backdrop: ${capitalize(variant)}`"      :variant="variant"    >      <Button variant="secondary">        {{ capitalize(variant) }}      </Button>      <template #body>        <p>          This drawer uses the <code>{{ variant }}</code> backdrop variant.        </p>      </template>      <template #footer="{ close }">        <Button class="w-full" @click="close">          Close        </Button>      </template>    </Drawer>  </div></template>

Customization

Tailwind CSS

<script setup lang="ts">import { Button, Drawer } from '@hareui/vue'</script><template>  <Drawer    class="border-l border-border/80 bg-surface"    :close-button="false"    placement="right"    title="Filters"    :ui="{ heading: 'text-foreground' }"    variant="blur"  >    <Button variant="secondary">      Open filters    </Button>    <template #body>      <p class="text-sm text-muted">        Narrow results by status, owner, or date.      </p>    </template>    <template #footer="{ close }">      <Button variant="secondary" @click="close">        Cancel      </Button>      <Button @click="close">        Apply      </Button>    </template>  </Drawer></template>

Global Configuration

To customize the Drawer classes for the whole app, extend the drawer theme in createHareUI:

app.use(createHareUI({
  ui: {
    drawer: {
      slots: {
        backdrop: 'bg-gradient-to-br from-black/50 to-black/70',
        dialog: 'rounded-2xl border border-white/10 shadow-2xl',
        header: 'text-center',
        closeTrigger: 'rounded-full bg-white/10 hover:bg-white/20',
      },
    },
  },
}))

Styling Reference

ui keydata-slotElement
backdropdrawer-backdropOverlay backdrop behind the drawer (Reka DialogOverlay)
contentdrawer-contentPositioning wrapper for the drawer panel
dialogdrawer-dialogThe drawer panel itself (Reka DialogContent); class merges here
headerdrawer-headerHeader section for titles
headingdrawer-headingMain title text (Reka DialogTitle)
bodydrawer-bodyMain scrollable content area, rendered when #body is present
footerdrawer-footerFooter section for actions, rendered when #footer is present
handledrawer-handleVisual drag handle indicator (bar: data-slot="drawer-handle-bar"), rendered when handle is true
closeTriggerdrawer-close-triggerClose button, rendered when closeButton is true

Backdrop Variants

  • variant="opaque" - Opaque colored backdrop (default)
  • variant="blur" - Blurred backdrop with glass effect
  • variant="transparent" - Transparent backdrop (no overlay)

Placement Variants

  • placement="bottom" - Slides up from the bottom edge (default)
  • placement="top" - Slides down from the top edge (the handle renders below the footer)
  • placement="left" - Slides in from the left edge
  • placement="right" - Slides in from the right edge

Interactive States

  • Focus: :focus-visible - applied to the trigger and close button
  • Hover: :hover - applied to the close button on hover
  • Active: :active - applied to the trigger and close button when pressed
  • Entering / Exiting: data-[state=open] / data-[state=closed] on backdrop, content and dialog, mapped from HeroUI's [data-entering] / [data-exiting]. The panel slide is a CSS animation (Reka waits for animations, not transitions, before unmounting) with HeroUI's distance, durations (250ms in, 200ms out) and easing
  • Placement: data-placement="top|bottom|left|right" on content and dialog

API Reference

Props

PropTypeDefaultDescription
openbooleanundefinedThe controlled open state of the drawer. Use with v-model:open.
defaultOpenboolean-The open state of the drawer when it is initially rendered. Use when you do not need to control its open state.
titlestring-Title rendered in the header.
placement"top" | "bottom" | "right" | "left""bottom"Edge of the screen the drawer slides in from.
variant"opaque" | "blur" | "transparent"'opaque'Visual style of the backdrop.
dismissiblebooleantrueWhether clicking the backdrop or dragging the drawer towards its edge closes it.
keyboardDismissiblebooleantrueWhether pressing Escape closes the drawer.
handlebooleanfalseWhether the drag handle bar is rendered (at the top, or at the bottom for placement="top").
closeButtonbooleantrueWhether the default close button is rendered.
portalstring | boolean | HTMLElementtrueWhere the drawer is teleported. true teleports to body, false renders in place, a string or element teleports to that target.
uiComponentSlots<{ slots: { backdrop: string[]; content: string[]; dialog: string[]; header: string; heading: string; body: string[]; footer: string[]; handle: string[]; closeTrigger: string; }; variants: { placement: { bottom: { content: string; dialog: string[]; }; top: { content: string; dialog: string[]; handle: string; }; left: { content: string; dialog: string[]; }; right: { content: string; dialog: string[]; }; }; variant: { opaque: { backdrop: string; }; blur: { backdrop: string; }; transparent: { backdrop: string; }; }; }; defaultVariants: { placement: string; variant: string; }; }>-Per-slot class overrides.

Slots

SlotPropsDescription
default{ open: boolean; }The trigger element. Omit to control the drawer purely via v-model:open.
headeranyReplaces the whole header (title).
titleanyReplaces the title text.
body{ close: () => void; }The drawer body (scrollable; drag-to-dismiss is excluded from it).
footer{ close: () => void; }The drawer footer (actions).
close{ close: () => void; }Replaces the default close button.
content{ close: () => void; }Replaces the entire drawer content (handle, header, body, footer, close trigger).

Emits

EventPayloadDescription
update:open[value: boolean]-

Accessibility

Implements the WAI-ARIA Dialog pattern via Reka UI's Dialog primitive:

  • Focus trap: focus is locked within the drawer while open
  • Keyboard: Escape closes it (unless keyboardDismissible is false), Tab cycles elements
  • Screen readers: the dialog always has an accessible name, either the rendered title or a visually hidden one
  • Scroll lock: body scroll is disabled while open
  • Drag to dismiss: pointer drags towards the drawer's edge on the handle, header and footer areas close it (when dismissible); controls and the body are excluded

On this page

No Headings