Componentfeedback
Popover
Floating content panel anchored to an interactive trigger.
Preview
Live Preview
Installation
Tokens guide →Add to your project via CLI (zero dependencies, code you own):
npx bigbullui add popoverOr install the full npm package:
npm install bigbulluiUsage
import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover";
<Popover>
<PopoverTrigger>
<Button variant="outline">Info</Button>
</PopoverTrigger>
<PopoverContent>Gate information.</PopoverContent>
</Popover>Source code
popover.tsxZero dependencies (only React + utils). Copy and paste directly into your project:
"use client";
import * as React from "react";
import { cn } from "./lib/utils";
interface PopoverContextValue {
open: boolean;
setOpen: (open: boolean) => void;
triggerRef: React.RefObject<HTMLButtonElement | null>;
}
const PopoverCtx = React.createContext<PopoverContextValue | null>(null);
export interface PopoverProps {
open?: boolean;
defaultOpen?: boolean;
onOpenChange?: (open: boolean) => void;
children: React.ReactNode;
}
export function Popover({
open: controlledOpen,
defaultOpen = false,
onOpenChange,
children,
}: PopoverProps) {
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen);
const isControlled = controlledOpen !== undefined;
const open = isControlled ? controlledOpen : uncontrolledOpen;
const triggerRef = React.useRef<HTMLButtonElement | null>(null);
const setOpen = React.useCallback(
(nextOpen: boolean) => {
if (!isControlled) {
setUncontrolledOpen(nextOpen);
}
onOpenChange?.(nextOpen);
},
[isControlled, onOpenChange]
);
return (
<PopoverCtx.Provider value={{ open, setOpen, triggerRef }}>
<div className="relative inline-block">{children}</div>
</PopoverCtx.Provider>
);
}
export interface PopoverTriggerProps {
children: React.ReactElement<{
onClick?: React.MouseEventHandler;
"aria-haspopup"?: string;
"aria-expanded"?: boolean;
}>;
}
export function PopoverTrigger({ children }: PopoverTriggerProps) {
const ctx = React.useContext(PopoverCtx);
if (!ctx) throw new Error("PopoverTrigger must be used within Popover");
return React.cloneElement(children, {
"aria-haspopup": "dialog",
"aria-expanded": ctx.open,
onClick: (e: React.MouseEvent) => {
children.props.onClick?.(e);
ctx.setOpen(!ctx.open);
},
});
}
export interface PopoverContentProps extends React.HTMLAttributes<HTMLDivElement> {
align?: "start" | "center" | "end";
children: React.ReactNode;
className?: string;
}
export function PopoverContent({
align = "center",
children,
className,
...props
}: PopoverContentProps) {
const ctx = React.useContext(PopoverCtx);
const contentRef = React.useRef<HTMLDivElement>(null);
React.useEffect(() => {
if (!ctx?.open) return;
function handleClickOutside(event: MouseEvent) {
if (contentRef.current && !contentRef.current.contains(event.target as Node)) {
ctx?.setOpen(false);
}
}
function handleKeyDown(event: KeyboardEvent) {
if (event.key === "Escape") {
ctx?.setOpen(false);
}
}
document.addEventListener("mousedown", handleClickOutside);
document.addEventListener("keydown", handleKeyDown);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
document.removeEventListener("keydown", handleKeyDown);
};
}, [ctx]);
if (!ctx?.open) return null;
return (
<div
ref={contentRef}
role="dialog"
aria-modal="true"
className={cn(
"absolute z-50 mt-2 w-72 rounded-lg border-[1.5px] border-foreground bg-card p-4 shadow-lg outline-1 outline-dashed outline-offset-[-6px] animate-[scale-in_0.15s_ease-out_both]",
align === "start" && "left-0",
align === "center" && "left-1/2 -translate-x-1/2",
align === "end" && "right-0",
className
)}
{...props}
>
{children}
</div>
);
}
Props
| Prop | Type | Description |
|---|---|---|
| open | boolean | Controlled open state. |
| defaultOpen | boolean | Initial open state. |
| onOpenChange | (open: boolean) => void | Open state callback. |