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

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>
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" and aria-modal="true" on the panel
  • The panel should have aria-labelledby pointing to the modal title id
  • Focus must be trapped inside the modal while it is open — Headless UI's Dialog does this automatically
  • Press Escape should always close the modal
  • Return focus to the trigger element when the modal closes

Released under the MIT License.