Shadcn Popover for React and Tailwind
Overlay with extra info or options on trigger.
Installation
bunx --bun shadcn@latest add https://kit.dev/r/popover.jsonpnpm dlx shadcn@latest add https://kit.dev/r/popover.jsonnpx shadcn@latest add https://kit.dev/r/popover.jsonyarn shadcn@latest add https://kit.dev/r/popover.json<Step>This component depends on Button and Scroll Area. Install them first if you haven't already.</Step>
Install the following dependencies:
bun add @ark-ui/react lucide-reactpnpm add @ark-ui/react lucide-reactnpm install @ark-ui/react lucide-reactyarn add @ark-ui/react lucide-reactCopy and paste the following code into your project.
"use client";
import { ark } from "@ark-ui/react/factory";
import {
Popover as ArkPopover,
usePopoverContext,
} from "@ark-ui/react/popover";
import { Portal } from "@ark-ui/react/portal";
import { XIcon } from "lucide-react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
export const usePopover = usePopoverContext;
export const Popover = (
props: React.ComponentProps<typeof ArkPopover.Root>
) => {
const {
lazyMount = true,
unmountOnExit = true,
modal = true,
...rest
} = props;
return (
<ArkPopover.Root
data-slot="popover"
lazyMount={lazyMount}
modal={modal}
unmountOnExit={unmountOnExit}
{...rest}
/>
);
};
export const PopoverTrigger = (
props: React.ComponentProps<typeof ArkPopover.Trigger>
) => <ArkPopover.Trigger data-slot="popover-trigger" {...props} />;
export const PopoverAnchor = (
props: React.ComponentProps<typeof ArkPopover.Anchor>
) => <ArkPopover.Anchor data-slot="popover-anchor" {...props} />;
export const PopoverPositioner = (
props: React.ComponentProps<typeof ArkPopover.Positioner>
) => <ArkPopover.Positioner data-slot="popover-positioner" {...props} />;
interface PopoverContentProps
extends React.ComponentProps<typeof ArkPopover.Content> {
/**
* Show close button at the top right corner
*
* @default true
*/
showCloseButton?: boolean;
}
export const PopoverContent = (props: PopoverContentProps) => {
const { showCloseButton = false, className, children, ...rest } = props;
return (
<Portal>
<PopoverPositioner>
<ArkPopover.Content
className={cn(
"relative",
"z-[calc(50+var(--layer-index,0))]",
"[--space:--spacing(4)]",
"w-auto min-w-32",
"flex flex-col",
"bg-popover",
"text-popover-foreground",
"rounded-xl border shadow-lg/5",
"outline-hidden",
"origin-(--transform-origin)",
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
"data-[state=closed]:zoom-out-[98%] data-[state=open]:zoom-in-[98%]",
"data-[state=closed]:animate-out data-[state=open]:animate-in",
"data-[placement=bottom]:slide-in-from-top-2",
"data-[placement=left]:slide-in-from-end-2",
"data-[placement=right]:slide-in-from-start-2",
"data-[placement=top]:slide-in-from-bottom-2",
"motion-reduce:animate-none!",
className
)}
data-slot="popover-content"
{...rest}
>
{children}
{!!showCloseButton && (
<PopoverClose asChild>
<Button
aria-label="Close"
className="absolute inset-e-2 top-2 opacity-64 hover:opacity-100"
size="icon-sm"
variant="ghost"
>
<XIcon />
</Button>
</PopoverClose>
)}
</ArkPopover.Content>
</PopoverPositioner>
</Portal>
);
};
interface PopoverHeaderProps extends React.ComponentProps<typeof ark.div> {
/**
* The description of the popover header
*/
description?: string;
/**
* The title of the popover header
*/
title?: string;
}
export const PopoverHeader = (props: PopoverHeaderProps) => {
const { title, description, children, className, ...rest } = props;
return (
<ark.div
className={cn(
"flex flex-col gap-2 p-(--space)",
"in-[[data-slot=popover-content]:has([data-slot=popover-body])]:pb-3",
className
)}
data-slot="popover-header"
{...rest}
>
{!!title && <PopoverTitle>{title}</PopoverTitle>}
{!!description && <PopoverDescription>{description}</PopoverDescription>}
{!title && typeof children === "string" ? (
<PopoverTitle>{children}</PopoverTitle>
) : (
children
)}
</ark.div>
);
};
export const PopoverTitle = (
props: React.ComponentProps<typeof ArkPopover.Title>
) => {
const { className, ...rest } = props;
return (
<ArkPopover.Title
className={cn("font-semibold text-base leading-none", className)}
data-slot="popover-title"
{...rest}
/>
);
};
export const PopoverDescription = (
props: React.ComponentProps<typeof ArkPopover.Description>
) => {
const { className, ...rest } = props;
return (
<ArkPopover.Description
className={cn("text-muted-foreground text-sm", className)}
data-slot="popover-description"
{...rest}
/>
);
};
export const PopoverBody = (props: React.ComponentProps<typeof ark.div>) => {
const { className, ...rest } = props;
return (
<ScrollArea>
<ark.div
className={cn(
"flex-1",
"p-(--space)",
"overflow-auto",
"in-[[data-slot=popover-content]:has([data-slot=popover-header])]:pt-1",
"in-[[data-slot=popover-content]:has([data-slot=popover-footer]:not(.border-t))]:pb-1",
className
)}
data-slot="popover-body"
{...rest}
/>
</ScrollArea>
);
};
export const PopoverFooter = (props: React.ComponentProps<typeof ark.div>) => {
const { className, ...rest } = props;
return (
<ark.div
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
"sm:rounded-b-[calc(var(--radius-lg)-1px)]",
"px-(--space) py-4",
"bg-muted/64",
"border-t",
className
)}
data-slot="popover-footer"
{...rest}
/>
);
};
export const PopoverClose = (
props: React.ComponentProps<typeof ArkPopover.CloseTrigger>
) => <ArkPopover.CloseTrigger data-slot="popover-close-trigger" {...props} />;
export const PopoverArrow = (
props: React.ComponentProps<typeof ArkPopover.Arrow>
) => {
const { style, ...rest } = props;
return (
<ArkPopover.Arrow
data-slot="popover-arrow"
style={
{
"--arrow-background": "var(--popover)",
"--arrow-size": "calc(1.5 * var(--spacing))",
...style,
} as React.CSSProperties
}
{...rest}
>
<ArkPopover.ArrowTip className="border-s border-t" />
</ArkPopover.Arrow>
);
};Update the import paths to match your project setup.
Anatomy
Popover
├── PopoverTrigger
└── PopoverContent
├── PopoverHeader
│ ├── PopoverTitle
│ └── PopoverDescription
├── PopoverBody
├── PopoverFooter
└── PopoverCloseUsage
import {
Popover,
PopoverTrigger,
PopoverContent,
PopoverHeader,
PopoverTitle,
PopoverDescription,
PopoverBody,
PopoverFooter,
PopoverClose,
} from "@/components/ui/popover";<Popover>
<PopoverTrigger />
<PopoverContent>
<PopoverHeader>
<PopoverTitle />
<PopoverDescription />
</PopoverHeader>
<PopoverBody>
{/* Your content here */}
</PopoverBody>
<PopoverFooter>
<PopoverClose />
</PopoverFooter>
</PopoverContent>
</Popover>Controlled
Use open and onOpenChange on the root to control the popover state.
Positioning
Control the position of the popover relative to the trigger using the positioning prop.
Title & Description
PopoverHeader supports two usage patterns:
Using props
Pass title and description props directly to PopoverHeader.
<PopoverHeader title="Popover Title" description="Popover description" />This approach does not require
PopoverTitleorPopoverDescriptioncomponents.
Using components
Use PopoverTitle and PopoverDescription as children for more control.
<PopoverHeader>
<PopoverTitle>Popover Title</PopoverTitle>
<PopoverDescription>Popover description</PopoverDescription>
</PopoverHeader>Examples
Non-modal
To make the popover non-modal, set the modal prop to false.
Nested
Nest popovers within one another.
Anchor
Use PopoverAnchor to position the popover relative to a different element than the trigger.
Close button
Use showCloseButton prop to show a close button in the top-right corner.
Close behavior
Use closeOnInteractOutside and closeOnEscape props to prevent closing on outside click and escape.
Scrollable
Use PopoverBody to make the content area scrollable while keeping header and footer fixed.
Inside dialog
Render a popover inside a dialog.
Custom spacing
Use [--space:--spacing("value")] on PopoverContent to adjust internal padding.
Default spacing is --spacing(4).
You can use breakpoint utilities to change the internal spacing at different screen sizes.
md:[--space:--spacing(6)] lg:[--space:--spacing(8)]API Reference
Popover
Root element of the popover.
| Prop | Type | Default |
|---|---|---|
open | boolean | - |
defaultOpen | boolean | - |
onOpenChange | (details: OpenChangeDetails) => void | - |
positioning | PositioningOptions | - |
modal | boolean | true |
closeOnInteractOutside | boolean | true |
closeOnEscape | boolean | true |
PopoverTrigger
Opens the popover on click. Use asChild for custom trigger elements.
| Prop | Type | Default |
|---|---|---|
asChild | boolean | - |
PopoverAnchor
Element the popover is positioned relative to. Use when the reference is not the trigger.
| Prop | Type | Default |
|---|---|---|
asChild | boolean | - |
PopoverContent
Holds the popover panel content. Displayed in a portal.
| Prop | Type | Default |
|---|---|---|
showCloseButton | boolean | false |
className | string | - |
| Attribute | Default |
|---|---|
--space | --spacing(4) |
PopoverHeader
Header container. Accepts title and description props or children.
| Prop | Type | Default |
|---|---|---|
title | string | - |
description | string | - |
className | string | - |
PopoverTitle
Accessible title for the popover panel.
| Prop | Type | Default |
|---|---|---|
asChild | boolean | - |
className | string | - |
PopoverDescription
Accessible description for the popover panel.
| Prop | Type | Default |
|---|---|---|
asChild | boolean | - |
className | string | - |
PopoverBody
Scrollable content area. Uses ScrollArea for overflow.
| Prop | Type | Default |
|---|---|---|
className | string | - |
PopoverFooter
Footer area for actions or secondary content.
| Prop | Type | Default |
|---|---|---|
className | string | - |
PopoverClose
Closes the popover on click. Use asChild for custom close elements.
| Prop | Type | Default |
|---|---|---|
asChild | boolean | - |
PopoverArrow
Optional arrow pointing toward the trigger element.
| Prop | Type | Default |
|---|---|---|
className | string | - |
| Attribute | Default |
|---|---|
--arrow-background | var(--popover) |
--arrow-size | calc(1.5 * var(--spacing)) |
For a complete list of props, see the Ark UI documentation.