Popover
Floating content panel triggered by a button — supports click-outside and keyboard dismiss for info cards, forms, and contextual menus.
Preview
Basic popover
import { Popover, PopoverButton, PopoverPanel } from '@/registry/ui-kit/tailwind-popover/react'
function Example() {
return (
<Popover>
<PopoverButton className="rounded-lg bg-indigo-600 px-4 py-2 text-sm font-medium text-white">
View details
</PopoverButton>
<PopoverPanel>
<p className="text-sm text-zinc-600">Popover content goes here.</p>
</PopoverPanel>
</Popover>
)
}Component API
| Prop | Default | Description |
|---|---|---|
Popover | — | Wrapper that manages open/close state, click-outside, and Escape key. |
PopoverButton | — | Trigger that toggles the popover panel. |
PopoverPanel | — | Positioned content container rendered below the trigger. |
Source Code
"use client";
import React, { useState, useRef, useEffect, createContext, useContext } from "react";
interface PopoverContextValue {
open: boolean;
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
}
const PopoverContext = createContext<PopoverContextValue>({
open: false,
setOpen: () => {},
});
/* ── Popover ────────────────────────────────────────── */
export function Popover({
children,
className = "",
}: {
children: React.ReactNode;
className?: string;
}) {
const [open, setOpen] = useState(false);
const ref = useRef<HTMLDivElement>(null);
// Click outside
useEffect(() => {
function handleClick(e: MouseEvent) {
if (ref.current && !ref.current.contains(e.target as Node)) {
setOpen(false);
}
}
document.addEventListener("mousedown", handleClick);
return () => document.removeEventListener("mousedown", handleClick);
}, []);
// Escape key
useEffect(() => {
function handleKey(e: KeyboardEvent) {
if (e.key === "Escape") setOpen(false);
}
if (open) {
document.addEventListener("keydown", handleKey);
return () => document.removeEventListener("keydown", handleKey);
}
}, [open]);
return (
<PopoverContext.Provider value={{ open, setOpen }}>
<div ref={ref} className={`relative ${className}`}>
{children}
</div>
</PopoverContext.Provider>
);
}
/* ── PopoverButton ──────────────────────────────────── */
export function PopoverButton({
children,
className = "",
}: {
children: React.ReactNode;
className?: string;
}) {
const { open, setOpen } = useContext(PopoverContext);
return (
<button
type="button"
onClick={() => setOpen(!open)}
aria-expanded={open}
className={className}
>
{children}
</button>
);
}
/* ── PopoverPanel ───────────────────────────────────── */
export function PopoverPanel({
children,
className = "",
}: {
children: React.ReactNode;
className?: string;
}) {
const { open } = useContext(PopoverContext);
if (!open) return null;
return (
<div
className={`absolute z-10 mt-2 w-72 rounded-lg border border-zinc-200 bg-white p-4 shadow-lg dark:border-zinc-700 dark:bg-zinc-900 ${className}`}
>
{children}
</div>
);
}