🔥 Oxide UI v0.1.0 — Dark mode only, copy-paste ready. Get started →
Skip to content

Drawer

<div role="dialog"> + Headless UI Dialog

A panel that slides in from the left or right edge of the viewport. Used for mobile navigation, settings panels, and detail views. Built with <Teleport> and <Transition> — or Headless UI Dialog for full accessibility.

Overview

Preview

Structure

A drawer has three parts: the backdrop (click to close), the animated panel, and an optional header/footer.

html
<!-- Backdrop -->
<div class="fixed inset-0 z-50 bg-black/60 backdrop-blur-sm" @click="open = false"></div>

<!-- Right panel -->
<div class="fixed inset-y-0 right-0 z-50 flex h-full w-80 flex-col border-l border-zinc-800 bg-zinc-950 shadow-2xl">
  <!-- Header -->
  <div class="flex items-center justify-between border-b border-zinc-800 px-5 py-4">
    <h2 class="text-sm font-semibold text-zinc-100">Settings</h2>
    <button @click="open = false" class="rounded-md p-1 text-zinc-500 hover:bg-zinc-800 hover:text-zinc-300">✕</button>
  </div>
  <!-- Body -->
  <div class="flex-1 overflow-y-auto px-5 py-4"><!-- content --></div>
  <!-- Footer -->
  <div class="border-t border-zinc-800 px-5 py-4 flex justify-end gap-2">
    <button class="...">Cancel</button>
    <button class="...">Save</button>
  </div>
</div>

Right drawer (Vue + Teleport)

vue
<script setup>
import { ref } from 'vue'
const open = ref(false)
</script>

<template>
  <button @click="open = true" class="rounded-md bg-[#D40C37] px-4 py-2 text-sm font-medium text-white hover:bg-[#b50a2f] transition-colors">
    Open settings
  </button>

  <Teleport to="body">
    <Transition
      enter-active-class="transition ease-out duration-300"
      enter-from-class="opacity-0"
      enter-to-class="opacity-100"
      leave-active-class="transition ease-in duration-200"
      leave-from-class="opacity-100"
      leave-to-class="opacity-0"
    >
      <div v-if="open" class="fixed inset-0 z-50 flex justify-end" role="dialog" aria-modal="true">
        <!-- Backdrop -->
        <div class="absolute inset-0 bg-black/60 backdrop-blur-sm" @click="open = false" aria-hidden="true"></div>

        <!-- Panel (slides in separately) -->
        <Transition
          enter-active-class="transition ease-out duration-300"
          enter-from-class="translate-x-full"
          enter-to-class="translate-x-0"
          leave-active-class="transition ease-in duration-200"
          leave-from-class="translate-x-0"
          leave-to-class="translate-x-full"
        >
          <div v-if="open" class="relative z-10 flex h-full w-80 flex-col border-l border-zinc-800 bg-zinc-950 shadow-2xl">
            <div class="flex items-center justify-between border-b border-zinc-800 px-5 py-4">
              <h2 class="text-sm font-semibold text-zinc-100">Settings</h2>
              <button @click="open = false" class="rounded-md p-1 text-zinc-500 hover:bg-zinc-800 hover:text-zinc-300 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-zinc-500">
                <svg class="h-4 w-4" ...>✕</svg>
              </button>
            </div>
            <div class="flex-1 overflow-y-auto px-5 py-4">
              <!-- drawer body -->
            </div>
            <div class="border-t border-zinc-800 px-5 py-4 flex justify-end gap-2">
              <button @click="open = false" class="rounded-md border border-zinc-700 px-4 py-2 text-sm text-zinc-300 hover:bg-zinc-800 transition-colors">Cancel</button>
              <button @click="open = false" class="rounded-md bg-[#D40C37] px-4 py-2 text-sm font-medium text-white hover:bg-[#b50a2f] transition-colors">Save</button>
            </div>
          </div>
        </Transition>
      </div>
    </Transition>
  </Teleport>
</template>

Left drawer (mobile nav)

Change justify-endjustify-start, slide direction from translate-x-full-translate-x-full:

vue
<div class="fixed inset-0 z-50 flex justify-start" ...>
  <Transition
    enter-from-class="-translate-x-full"
    enter-to-class="translate-x-0"
    leave-from-class="translate-x-0"
    leave-to-class="-translate-x-full"
    ...
  >
    <div v-if="open" class="relative z-10 flex h-full w-72 flex-col border-r border-zinc-800 bg-zinc-950">
      <!-- nav content -->
    </div>
  </Transition>
</div>

Using Dialog gives you automatic focus trapping, aria-modal, and Esc-to-close:

vue
<script setup>
import { Dialog, DialogPanel, TransitionRoot, TransitionChild } from '@headlessui/vue'
import { ref } from 'vue'
const open = ref(false)
</script>

<template>
  <button @click="open = true" class="...">Open settings</button>

  <TransitionRoot :show="open" as="template">
    <Dialog @close="open = false" class="relative z-50">

      <!-- Backdrop -->
      <TransitionChild
        enter="transition ease-out duration-300" enter-from="opacity-0" enter-to="opacity-100"
        leave="transition ease-in duration-200" leave-from="opacity-100" leave-to="opacity-0"
      >
        <div class="fixed inset-0 bg-black/60 backdrop-blur-sm" aria-hidden="true" />
      </TransitionChild>

      <!-- Panel -->
      <div class="fixed inset-y-0 right-0 flex">
        <TransitionChild
          enter="transition ease-out duration-300" enter-from="translate-x-full" enter-to="translate-x-0"
          leave="transition ease-in duration-200" leave-from="translate-x-0" leave-to="translate-x-full"
        >
          <DialogPanel class="flex h-full w-80 flex-col border-l border-zinc-800 bg-zinc-950 shadow-2xl">
            <div class="flex items-center justify-between border-b border-zinc-800 px-5 py-4">
              <h2 class="text-sm font-semibold text-zinc-100">Settings</h2>
              <button @click="open = false" class="rounded-md p-1 text-zinc-500 hover:bg-zinc-800 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-zinc-500">✕</button>
            </div>
            <div class="flex-1 overflow-y-auto px-5 py-4"><!-- content --></div>
          </DialogPanel>
        </TransitionChild>
      </div>

    </Dialog>
  </TransitionRoot>
</template>

Sizes

html
<!-- Narrow -->    <div class="w-64 ...">
<!-- Default -->   <div class="w-80 ...">
<!-- Wide -->      <div class="w-96 ...">
<!-- Full sheet --> <div class="w-full max-w-lg ...">

Accessibility notes

  • Always use role="dialog" and aria-modal="true" on the panel — or use DialogPanel from Headless UI which sets these automatically
  • Focus must be trapped inside the drawer while open; Headless UI Dialog handles this automatically
  • Esc key must close the drawer; Headless UI's @close handles this automatically
  • When the drawer closes, focus must return to the element that opened it

Released under the MIT License.