Modal / Dialog
<div role="dialog">
A focused overlay panel that requires user interaction before returning to the page. Use for confirmations, multi-step forms, and detail views.
Overview
Preview
Structure
html
<!-- Backdrop -->
<div class="fixed inset-0 z-50 flex items-center justify-center p-4">
<div class="absolute inset-0 bg-black/70 backdrop-blur-sm" />
<!-- Panel -->
<div role="dialog" aria-modal="true" aria-labelledby="modal-title"
class="relative z-10 w-full max-w-md overflow-hidden rounded-2xl border border-zinc-700/60 bg-zinc-900 shadow-2xl shadow-black/60"
>
<!-- Header -->
<div class="flex items-start justify-between border-b border-zinc-800 px-6 py-5">
<div>
<h2 id="modal-title" class="text-base font-semibold text-zinc-100">Modal title</h2>
<p class="mt-0.5 text-sm text-zinc-500">A short supporting description.</p>
</div>
<button aria-label="Close" class="rounded-md p-1 text-zinc-500 hover:bg-zinc-800 hover:text-zinc-300 focus:outline-none">
<svg class="h-4 w-4" ...>✕</svg>
</button>
</div>
<!-- Body -->
<div class="px-6 py-5">
<p class="text-sm text-zinc-400 leading-relaxed">Modal body content.</p>
</div>
<!-- Footer -->
<div class="flex items-center justify-end gap-3 border-t border-zinc-800 px-6 py-4">
<button class="rounded-md border border-zinc-700 bg-transparent px-4 py-2 text-sm font-medium text-zinc-300 hover:bg-zinc-800">
Cancel
</button>
<button class="rounded-md bg-[#D40C37] px-4 py-2 text-sm font-medium text-white hover:bg-[#b50a2f]">
Confirm
</button>
</div>
</div>
</div>With Vue reactivity + Teleport
vue
<script setup>
import { ref } from 'vue'
const open = ref(false)
</script>
<template>
<button @click="open = true" class="...">Open</button>
<Teleport to="body">
<Transition enter-active-class="transition ease-out duration-200" enter-from-class="opacity-0" enter-to-class="opacity-100">
<div v-if="open" class="fixed inset-0 z-50 flex items-center justify-center p-4">
<div class="absolute inset-0 bg-black/70 backdrop-blur-sm" @click="open = false" />
<div role="dialog" aria-modal="true" class="relative z-10 w-full max-w-md rounded-2xl border border-zinc-700/60 bg-zinc-900 shadow-2xl">
<!-- content -->
</div>
</div>
</Transition>
</Teleport>
</template>With Headless UI (recommended for production)
vue
<script setup>
import { Dialog, DialogPanel, DialogTitle } from '@headlessui/vue'
import { ref } from 'vue'
const isOpen = ref(false)
</script>
<template>
<Dialog :open="isOpen" @close="isOpen = false" class="relative z-50">
<div class="fixed inset-0 bg-black/70 backdrop-blur-sm" aria-hidden="true" />
<div class="fixed inset-0 flex items-center justify-center p-4">
<DialogPanel class="w-full max-w-md rounded-2xl border border-zinc-700/60 bg-zinc-900 shadow-2xl">
<DialogTitle class="px-6 py-5 text-base font-semibold text-zinc-100 border-b border-zinc-800">
Modal title
</DialogTitle>
<div class="px-6 py-5">Content</div>
</DialogPanel>
</div>
</Dialog>
</template>Sizes
html
<div class="... w-full max-w-sm ..."> <!-- Small -->
<div class="... w-full max-w-md ..."> <!-- Medium (default) -->
<div class="... w-full max-w-lg ..."> <!-- Large -->
<div class="... w-full max-w-2xl ..."> <!-- XL -->Accessibility notes
- Always use
role="dialog"andaria-modal="true"on the panel - The panel should have
aria-labelledbypointing to the modal titleid - Focus must be trapped inside the modal while it is open — Headless UI's
Dialogdoes this automatically - Press
Escapeshould always close the modal - Return focus to the trigger element when the modal closes