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";
export const Example1: React.FC = () => {
return (
<Popover>
<PopoverTrigger>
<Button>Open popover</Button>
</PopoverTrigger>
<PopoverContent className="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 { Button } from "@repo/ui/components/button";
import {
Popover,
PopoverBody,
PopoverContent,
PopoverTrigger,
} from "@repo/ui/components/popover";
export const Example2: React.FC = () => {
return (
<div className="grid grid-cols-2 gap-5">
<Popover>
<PopoverTrigger>
<Button>Top</Button>
</PopoverTrigger>
<PopoverContent showArrow placement="top">
<PopoverBody>This is TOP popover content</PopoverBody>
</PopoverContent>
</Popover>
<Popover>
<PopoverTrigger>
<Button>Right</Button>
</PopoverTrigger>
<PopoverContent showArrow placement="right">
<PopoverBody>This is RIGHT popover content</PopoverBody>
</PopoverContent>
</Popover>
<Popover>
<PopoverTrigger>
<Button>Left</Button>
</PopoverTrigger>
<PopoverContent showArrow placement="left">
<PopoverBody>This is LEFT popover content</PopoverBody>
</PopoverContent>
</Popover>
<Popover>
<PopoverTrigger>
<Button>Bottom</Button>
</PopoverTrigger>
<PopoverContent showArrow 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";
export const Example3: React.FC = () => {
return (
<div className="flex min-h-[400px] items-center justify-center">
<Popover>
<PopoverTrigger>
<Button>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 {
OverlayArrow,
Popover as AriaPopover,
PopoverProps as AriaPopoverProps,
DialogTrigger,
DialogTriggerProps,
composeRenderProps,
PopoverContext,
useSlottedContext,
Dialog,
} from "react-aria-components";
import React from "react";
import { tv, cn } from "tailwind-variants";
const popoverStyles = tv({
base: "bg-popover text-popover-foreground z-50 rounded-md border p-4 shadow-md outline-none",
variants: {
isEntering: {
true: "animate-in fade-in-0 zoom-in-95 duration-200",
},
isExiting: {
true: "animate-out fade-out-0 zoom-out-95 duration-150",
},
},
});
// ============================================================================
// Popover (Wrapper)
// ============================================================================
export const Popover: React.FC<DialogTriggerProps> = (props) => {
return <DialogTrigger {...props} />;
};
// ============================================================================
// PopoverTrigger
// ============================================================================
export const PopoverTrigger: React.FC<React.PropsWithChildren> = ({
children,
}) => {
return children;
};
// ============================================================================
// PopoverContent
// ============================================================================
export interface PopoverContentProps
extends Omit<AriaPopoverProps, "children"> {
showArrow?: boolean;
children: React.ReactNode;
}
export const PopoverContent: React.FC<PopoverContentProps> = ({
children,
showArrow,
className,
...props
}) => {
const popoverContext = useSlottedContext(PopoverContext);
const isSubmenu = popoverContext?.trigger === "SubmenuTrigger";
let offset = showArrow ? 12 : 8;
offset = isSubmenu ? offset - 6 : offset;
return (
<AriaPopover
data-slot="popover-content"
offset={offset}
{...props}
className={composeRenderProps(
className,
(className, renderProps) =>
cn(popoverStyles({ ...renderProps }), className) || "",
)}
>
{showArrow && (
<OverlayArrow className="group">
<svg
width={12}
height={12}
viewBox="0 0 12 12"
className="fill-popover stroke-border block group-data-[placement^=bottom]:rotate-180 group-data-[placement^=left]:-rotate-90 group-data-[placement^=right]:rotate-90"
>
<path d="M0 0 L6 6 L12 0" />
</svg>
</OverlayArrow>
)}
<Dialog className="outline-none">{children}</Dialog>
</AriaPopover>
);
};
// ============================================================================
// PopoverHeader
// ============================================================================
export interface PopoverHeaderProps extends React.ComponentProps<"div"> {}
export const PopoverHeader: React.FC<PopoverHeaderProps> = ({
children,
className,
}) => {
return (
<div className={cn("pb-2", className)}>
<h3 className="text-sm font-semibold leading-none">{children}</h3>
</div>
);
};
// ============================================================================
// PopoverBody
// ============================================================================
export interface PopoverBodyProps extends React.ComponentProps<"div"> {}
export const PopoverBody: React.FC<PopoverBodyProps> = ({
children,
className,
}) => {
return <div className={cn("py-2", className)}>{children}</div>;
};
// ============================================================================
// PopoverFooter
// ============================================================================
export interface PopoverFooterProps extends React.ComponentProps<"div"> {}
export const PopoverFooter: React.FC<PopoverFooterProps> = ({
children,
className,
}) => {
return <div className={cn("border-t pt-2", className)}>{children}</div>;
};