Componentfeedback1.28 KB gzipZero dependencies
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
Open in StackBlitzimport { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover";
<Popover>
<PopoverTrigger>
<Button variant="outline">Info</Button>
</PopoverTrigger>
<PopoverContent>Gate information.</PopoverContent>
</Popover>Usage guidance
Use Popover when a ticket stub surface needs Floating content panel anchored to an interactive trigger. Keep props minimal and prefer composition with sibling stubs.
Avoid Popover for long form flows or dense data grids where a dedicated layout block fits better.
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;
/** Dual-mode: false renders instantly with no motion. Default true. */
animated?: boolean;
}
export function PopoverContent({
align = "center",
children,
className,
animated = true,
...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]",
animated && "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 | |
| defaultOpen? | boolean | |
| onOpenChange? | (open: boolean) => void | |
| children | React.ReactNode |