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

Popover

<div> + Headless UI Popover

A floating panel anchored to a trigger element. Richer than a tooltip — can contain interactive content. Built on Headless UI Popover.

Overview

Preview

Basic popover

Preview

About this feature

This component is powered by Headless UI — fully accessible, keyboard navigable, and focus-managed.

html
<div class="relative inline-block">
  <!-- Trigger -->
  <button class="inline-flex items-center gap-2 rounded-md border border-zinc-700 bg-zinc-900 px-3 py-2 text-sm font-medium text-zinc-100 hover:bg-zinc-800 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-zinc-500">
    Info
    <svg class="h-3.5 w-3.5 text-zinc-500" ...></svg>
  </button>

  <!-- Panel -->
  <div class="absolute left-0 top-full z-20 mt-2 w-64 rounded-xl border border-zinc-700/60 bg-zinc-900 p-4 shadow-xl shadow-black/40">
    <p class="text-xs font-semibold text-zinc-200">About this feature</p>
    <p class="mt-1.5 text-xs leading-relaxed text-zinc-400">Popover content goes here.</p>
  </div>
</div>

Profile card popover

html
<div class="absolute right-0 top-full z-20 mt-2 w-56 rounded-xl border border-zinc-700/60 bg-zinc-900 py-2 shadow-xl shadow-black/40">
  <!-- User info -->
  <div class="border-b border-zinc-800 px-4 py-3">
    <p class="text-sm font-semibold text-zinc-100">Alex Morgan</p>
    <p class="text-xs text-zinc-500">alex@oxide.dev</p>
  </div>
  <!-- Nav items -->
  <div class="py-1">
    <button class="flex w-full items-center gap-2.5 px-4 py-2 text-sm text-zinc-300 hover:bg-zinc-800 transition-colors">Profile</button>
    <button class="flex w-full items-center gap-2.5 px-4 py-2 text-sm text-zinc-300 hover:bg-zinc-800 transition-colors">Settings</button>
  </div>
  <div class="border-t border-zinc-800 py-1">
    <button class="flex w-full items-center gap-2.5 px-4 py-2 text-sm text-red-400 hover:bg-red-950/30 transition-colors">Sign out</button>
  </div>
</div>

With Vue reactivity + outside click

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

function onClickOutside(e) {
  if (!e.target.closest('.popover-root')) open.value = false
}
onMounted(() => document.addEventListener('click', onClickOutside))
onUnmounted(() => document.removeEventListener('click', onClickOutside))
</script>

<template>
  <div class="popover-root relative inline-block">
    <button @click="open = !open" class="...">Trigger</button>
    <Transition
      enter-active-class="transition ease-out duration-100"
      enter-from-class="opacity-0 scale-95 translate-y-1"
      enter-to-class="opacity-100 scale-100 translate-y-0"
    >
      <div v-if="open" class="absolute z-20 ...">
        <!-- content -->
      </div>
    </Transition>
  </div>
</template>
vue
<script setup>
import { Popover, PopoverButton, PopoverPanel } from '@headlessui/vue'
</script>

<template>
  <Popover class="relative inline-block">
    <PopoverButton class="inline-flex items-center gap-2 rounded-md border border-zinc-700 bg-zinc-900 px-3 py-2 text-sm font-medium text-zinc-100 hover:bg-zinc-800 focus:outline-none focus-visible:ring-2 focus-visible:ring-zinc-500">
      Info
    </PopoverButton>
    <Transition
      enter-active-class="transition ease-out duration-100"
      enter-from-class="opacity-0 scale-95"
      enter-to-class="opacity-100 scale-100"
      leave-active-class="transition ease-in duration-75"
      leave-from-class="opacity-100 scale-100"
      leave-to-class="opacity-0 scale-95"
    >
      <PopoverPanel class="absolute left-0 z-20 mt-2 w-64 origin-top-left rounded-xl border border-zinc-700/60 bg-zinc-900 p-4 shadow-xl shadow-black/40 focus:outline-none">
        <p class="text-xs font-semibold text-zinc-200">About this feature</p>
        <p class="mt-1.5 text-xs leading-relaxed text-zinc-400">Content goes here.</p>
      </PopoverPanel>
    </Transition>
  </Popover>
</template>

Positioning

Control position with Tailwind placement classes on the panel:

html
<!-- Top -->   <div class="absolute bottom-full left-0 mb-2 ...">
<!-- Right -->  <div class="absolute left-full top-0 ml-2 ...">
<!-- Bottom (default) --> <div class="absolute top-full left-0 mt-2 ...">
<!-- Left -->  <div class="absolute right-full top-0 mr-2 ...">

For dynamic positioning based on viewport edges, use Floating UI with @floating-ui/vue.

Accessibility notes

  • PopoverButton sets aria-expanded automatically
  • PopoverPanel traps tab focus within the panel while open
  • Pressing Escape closes the panel and returns focus to the trigger
  • Clicking outside the panel closes it automatically

Released under the MIT License.