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-end → justify-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>With Headless UI Dialog (recommended for accessibility)
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"andaria-modal="true"on the panel — or useDialogPanelfrom Headless UI which sets these automatically - Focus must be trapped inside the drawer while open; Headless UI
Dialoghandles this automatically Esckey must close the drawer; Headless UI's@closehandles this automatically- When the drawer closes, focus must return to the element that opened it