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>With Headless UI Popover (recommended)
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
PopoverButtonsetsaria-expandedautomaticallyPopoverPaneltraps tab focus within the panel while open- Pressing
Escapecloses the panel and returns focus to the trigger - Clicking outside the panel closes it automatically