Popover
Generic anchored floating panel — auto-positioned, outside-click + Esc to close.
import { Popover }from "@/components/ui"
Trigger + panel
Full source
The complete component, read from src/components/ui/popover.tsx.
popover.tsx
"use client";
import { useRef, useEffect, type ReactNode } from "react";
import { createPortal } from "react-dom";
import { cn } from "@/lib/utils";
import { usePopoverPosition, useMounted, useOutsideClick } from "@/hooks";
interface PopoverProps {
open: boolean;
onOpenChange: (open: boolean) => void;
trigger: ReactNode;
children: ReactNode;
align?: "start" | "center" | "end";
minWidth?: number;
matchWidth?: boolean;
className?: string;
}
export function Popover({
open,
onOpenChange,
trigger,
children,
align = "start",
minWidth = 200,
matchWidth,
className,
}: PopoverProps) {
const anchorRef = useRef<HTMLSpanElement>(null);
const panelRef = useRef<HTMLDivElement>(null);
const mounted = useMounted();
const position = usePopoverPosition(anchorRef, open, { align, minWidth, matchWidth, preferredHeight: 280 });
useOutsideClick([anchorRef, panelRef], open, () => onOpenChange(false));
useEffect(() => {
if (!open) return;
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") onOpenChange(false);
};
document.addEventListener("keydown", onKey);
return () => document.removeEventListener("keydown", onKey);
}, [open, onOpenChange]);
return (
<span ref={anchorRef} className="relative inline-flex">
<span onClick={() => onOpenChange(!open)} className="contents">
{trigger}
</span>
{open && position && mounted &&
createPortal(
<div
ref={panelRef}
role="dialog"
className={cn(
"z-50 rounded-lg border border-border bg-surface p-3 shadow-lg outline-none",
className
)}
style={position.style}
>
{children}
</div>,
document.body
)}
</span>
);
}Dependencies
To use Popover you also need the following. Copy these into your project alongside it.
Recommended folder structure
project structure
src/components/ui/ └─ popover.tsx src/hooks/ ├─ use-mounted.ts └─ use-popover-position.ts src/lib/ └─ utils.ts