Popover
Displays rich content in a portal, triggered by a button.
Basic Usage
import { Button } from "@repo/ui/components/button";
import { Input } from "@repo/ui/components/input";
import { Label } from "@repo/ui/components/label";
import {
Popover,
PopoverBody,
PopoverContent,
PopoverTrigger,
} from "@repo/ui/components/popover";
interface Example1Props {}
export const Example1: React.FC<Example1Props> = ({}) => {
return (
<Popover>
<PopoverTrigger asChild>
<Button variant="outline">Open popover</Button>
</PopoverTrigger>
<PopoverContent className="bg-fd-card w-80">
<PopoverBody>
<div className="grid gap-4">
<div className="space-y-2">
<h4 className="font-medium leading-none">
Dimensions
</h4>
<p className="text-muted-foreground text-sm">
Set the dimensions for the layer.
</p>
</div>
<div className="grid gap-2">
<div className="grid grid-cols-3 items-center gap-4">
<Label htmlFor="width">Width</Label>
<Input
id="width"
defaultValue="100%"
className="col-span-2 h-8"
/>
</div>
<div className="grid grid-cols-3 items-center gap-4">
<Label htmlFor="maxWidth">Max. width</Label>
<Input
id="maxWidth"
defaultValue="300px"
className="col-span-2 h-8"
/>
</div>
<div className="grid grid-cols-3 items-center gap-4">
<Label htmlFor="height">Height</Label>
<Input
id="height"
defaultValue="25px"
className="col-span-2 h-8"
/>
</div>
<div className="grid grid-cols-3 items-center gap-4">
<Label htmlFor="maxHeight">Max. height</Label>
<Input
id="maxHeight"
defaultValue="none"
className="col-span-2 h-8"
/>
</div>
</div>
</div>
</PopoverBody>
</PopoverContent>
</Popover>
);
};
Directions of popover
"use client";
import {
Popover,
PopoverBody,
PopoverContent,
PopoverTrigger,
} from "@repo/ui/components/popover";
interface Example2Props {}
export const Example2: React.FC<Example2Props> = ({}) => {
return (
<>
<div className="grid grid-cols-2 gap-5">
<Popover>
<PopoverTrigger>Top</PopoverTrigger>
<PopoverContent placement="top">
<PopoverBody>This is TOP popover content</PopoverBody>
</PopoverContent>
</Popover>
{/* --------------------------------------------------- */}
<Popover>
<PopoverTrigger>Right</PopoverTrigger>
<PopoverContent placement="right">
<PopoverBody>This is RIGHT popover content</PopoverBody>
</PopoverContent>
</Popover>
{/* --------------------------------------------------- */}
<Popover>
<PopoverTrigger>Left</PopoverTrigger>
<PopoverContent placement="left">
<PopoverBody>This is LEFT popover content</PopoverBody>
</PopoverContent>
</Popover>
{/* --------------------------------------------------- */}
<Popover>
<PopoverTrigger>Bottom</PopoverTrigger>
<PopoverContent placement="bottom">
<PopoverBody>
This is BOTTOM popover content
</PopoverBody>
</PopoverContent>
</Popover>
</div>
</>
);
};
All Components
import { Button } from "@repo/ui/components/button";
import { Checkbox } from "@repo/ui/components/checkbox";
import {
Popover,
PopoverTrigger,
PopoverContent,
PopoverHeader,
PopoverBody,
PopoverFooter,
} from "@repo/ui/components/popover";
interface Example3Props {}
export const Example3: React.FC<Example3Props> = ({}) => {
return (
<div className="flex min-h-[400px] items-center justify-center">
<Popover>
<PopoverTrigger asChild>
<Button variant="outline">Open Popover</Button>
</PopoverTrigger>
<PopoverContent className="w-80">
<PopoverHeader>
<h3 className="text-lg font-semibold">
Account Settings
</h3>
</PopoverHeader>
<PopoverBody>
<p className="mb-4 text-sm text-gray-600">
Manage your account preferences and settings here.
Changes will be saved automatically.
</p>
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm">
Email notifications
</span>
<Checkbox defaultSelected />
</div>
<div className="flex items-center justify-between">
<span className="text-sm">
Marketing emails
</span>
<Checkbox />
</div>
</div>
</PopoverBody>
<PopoverFooter>
<div className="flex justify-end gap-2">
<Button variant="ghost" size="sm">
Cancel
</Button>
<Button size="sm">Save Changes</Button>
</div>
</PopoverFooter>
</PopoverContent>
</Popover>
</div>
);
};
Component Code
"use client";
import { useOverlayTriggerState, OverlayTriggerState } from "react-stately";
import {
useOverlayTrigger,
usePopover,
Overlay,
DismissButton,
AriaButtonProps,
AriaPopoverProps,
useButton,
mergeProps,
useOverlay,
} from "react-aria";
import {
createContext,
useContext,
useRef,
ReactNode,
forwardRef,
RefObject,
} from "react";
import { cn } from "@repo/ui/lib/utils";
import { Slot } from "@repo/ui/components/slot";
import { Button } from "@repo/ui/components/button";
interface PopoverProviderT {
defaultOpen?: boolean;
isOpen?: boolean;
onOpenChange?: (val: boolean) => void;
modal?: boolean;
}
// ============================================================================
// Popover Context
// ============================================================================
interface PopoverContextT extends PopoverProviderT {
state: OverlayTriggerState;
triggerRef: RefObject<HTMLButtonElement | null>;
}
const PopoverContext = createContext<PopoverContextT | null>(null);
const Popover = ({
children,
...props
}: PopoverProviderT & { children: ReactNode }) => {
const state = useOverlayTriggerState(props);
const triggerRef = useRef<HTMLButtonElement>(null);
return (
<PopoverContext.Provider value={{ state, triggerRef, ...props }}>
{children}
</PopoverContext.Provider>
);
};
// ============================================================================
// Popover Trigger
// ============================================================================
interface PopoverTriggerProps extends AriaButtonProps {
asChild?: boolean;
className?: string;
children: ReactNode;
}
const PopoverTrigger = forwardRef<HTMLButtonElement, PopoverTriggerProps>(
({ asChild, className, children, ...props }, ref) => {
const ctx = useContext(PopoverContext);
if (!ctx) throw new Error("PopoverTrigger must be inside <Popover>");
const { state, triggerRef } = ctx;
const { triggerProps } = useOverlayTrigger(
{ type: "dialog" },
state,
triggerRef,
);
const internalRef = useRef<HTMLButtonElement>(null);
const { buttonProps } = useButton(props, internalRef);
const finalProps = mergeProps(triggerProps, buttonProps);
const mergedRef = (node: HTMLButtonElement | null) => {
triggerRef.current = node;
internalRef.current = node;
if (typeof ref === "function") {
ref(node);
} else if (ref) {
/* eslint-disable */
(ref as any).current = node;
}
};
if (asChild) {
return (
<Slot {...finalProps} ref={mergedRef} className={className}>
{children}
</Slot>
);
}
return (
<Button
{...finalProps}
ref={mergedRef}
className={cn("outline-none", className)}
>
{children}
</Button>
);
},
);
PopoverTrigger.displayName = "PopoverTrigger";
// ============================================================================
// Popover Content
// ============================================================================
interface PopoverContentProps
extends Omit<AriaPopoverProps, "popoverRef" | "triggerRef"> {
className?: string;
children: ReactNode;
asChild?: boolean;
}
const PopoverContent = forwardRef<HTMLDivElement, PopoverContentProps>(
({ asChild, className, children, ...props }, ref) => {
const ctx = useContext(PopoverContext);
if (!ctx) throw new Error("PopoverContent must be inside <Popover>");
const { state, triggerRef } = ctx;
const popoverRef = useRef<HTMLDivElement>(null);
const mergedRef = (node: HTMLDivElement | null) => {
popoverRef.current = node;
if (typeof ref === "function") {
ref(node);
} else if (ref) {
/* eslint-disable */
(ref as any).current = node;
}
};
// 🔹 Add overlayProps for dismissal behavior
const { overlayProps } = useOverlay(
{
onClose: state.close,
isOpen: state.isOpen,
isDismissable: true,
shouldCloseOnBlur: true,
},
popoverRef,
);
const { popoverProps, underlayProps } = usePopover(
{
...props,
popoverRef,
triggerRef,
offset: props.offset ?? 8,
placement: props.placement ?? "bottom",
isNonModal: !ctx.modal,
},
state,
);
const dataState = state.isOpen ? "open" : "closed";
const side = props.placement?.split(" ")[0] ?? "bottom";
const animationClasses =
"data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95" +
" data-[side=bottom]:slide-in-from-top-2" +
" data-[side=top]:slide-in-from-bottom-2" +
" data-[side=left]:slide-in-from-right-2" +
" data-[side=right]:slide-in-from-left-2";
const content = asChild ? (
<Slot
{...mergeProps(popoverProps, overlayProps)}
ref={mergedRef}
data-state={dataState}
data-side={side}
className={cn(animationClasses, className)}
>
{children}
</Slot>
) : (
<div
id="testing"
{...mergeProps(popoverProps, overlayProps)}
ref={mergedRef}
data-state={dataState}
data-side={side}
className={cn(
animationClasses,
"bg-popover text-popover-foreground absolute z-50 w-72 rounded-md border p-4 shadow-md outline-none",
className,
)}
>
{children}
<DismissButton onDismiss={state.close} />
</div>
);
if (!state.isOpen) return null;
return (
<Overlay>
{!ctx.modal && (
<div {...underlayProps} className="fixed inset-0 z-40" />
)}
{content}
</Overlay>
);
},
);
PopoverContent.displayName = "PopoverContent";
// ============================================================================
// Popover Header (Optional)
// ============================================================================
interface PopoverHeaderProps {
children: React.ReactNode;
className?: string;
}
const PopoverHeader: React.FC<PopoverHeaderProps> = ({
children,
className,
}) => {
return (
<div className={cn("px-4 pb-2 pt-4", className)}>
<h3 className="text-sm font-semibold leading-none">{children}</h3>
</div>
);
};
// ============================================================================
// Popover Body (Optional)
// ============================================================================
interface PopoverBodyProps {
children: React.ReactNode;
className?: string;
}
const PopoverBody: React.FC<PopoverBodyProps> = ({ children, className }) => {
return <div className={cn("px-4 py-3", className)}>{children}</div>;
};
// ============================================================================
// Popover Footer (Optional)
// ============================================================================
interface PopoverFooterProps {
children: React.ReactNode;
className?: string;
}
const PopoverFooter: React.FC<PopoverFooterProps> = ({
children,
className,
}) => {
return (
<div
className={cn(
"flex items-center gap-2 border-t px-4 pb-4 pt-2",
className,
)}
>
{children}
</div>
);
};
export {
Popover,
PopoverTrigger,
PopoverContent,
PopoverHeader,
PopoverBody,
PopoverFooter,
};
// <Popover>
// <PopoverTrigger></PopoverTrigger>
// <PopoverContent>
// <PopoverHeader></PopoverHeader>
// <PopoverBody></PopoverBody>
// <PopoverFooter></PopoverFooter>
// </PopoverContent>
// </Popover>