Built on @radix-ui/react-popover — floating content anchored to a trigger. Used as the dropdown wrapper for Month Picker and Calendar, but works for any anchored content.
npx chandra-asri-ui add popoverPopoverTrigger renders its own real <button> by default — style it directly with buttonVariants instead of wrapping Button with asChild, since this bundle's Buttonis a plain function component and can't accept the ref asChild needs.
"use client";
import { buttonVariants } from "@/app/components/ui/button";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/app/components/ui/popover";
import { cn } from "@/lib/utils";
const SIDES = [
{ side: "top", label: "Top" },
{ side: "right", label: "Right" },
{ side: "bottom", label: "Bottom" },
{ side: "left", label: "Left" },
] as const;
/**
* One Popover per side, each forced open on that side via `side` — shows the
* arrow flipping direction to match wherever the content actually lands.
*/
export function PopoverDemo() {
return (
<div className="flex flex-wrap items-center justify-center gap-8 p-10">
{SIDES.map(({ side, label }) => (
<Popover key={side}>
<PopoverTrigger
className={cn(buttonVariants({ variant: "outline" }))}
>
{label}
</PopoverTrigger>
<PopoverContent side={side} className="w-48">
<p className="text-foreground text-sm">
Opens from the {label.toLowerCase()}.
</p>
</PopoverContent>
</Popover>
))}
</div>
);
}
PopoverContent always renders a small SVG arrow pointing back at the trigger — it's built in, not something you add separately. The four buttons above each force their popover open on a different side (top, right, bottom, left) to show the arrow flipping to match — Radix's Popper positioning rotates it automatically, and it also re-flips on its own if a popover doesn't have room on its preferred side (near a viewport edge, for example).
"use client";
import { cn } from "@/lib/utils";
import * as PopoverPrimitive from "@radix-ui/react-popover";
import * as React from "react";
/**
* Built on @radix-ui/react-popover — used as the dropdown wrapper for the
* Month Picker / Calendar trigger pattern. Like Select, the source relies on
* the `tailwindcss-animate` plugin (fade + zoom + slide-from-side) for its
* open/close transition; since that plugin isn't part of this bundle,
* `PopoverContent` instead gets a plain CSS keyframe animation
* (`.animate-popover-content` in globals.css) that reads Radix's own
* `data-state`/`data-side` attributes to reproduce the same fade+zoom+slide.
*
* `PopoverContent` always renders a `PopoverArrow` pointing back at the
* trigger — Radix's Popper positioning rotates it automatically to match
* whichever side/align the content actually lands on (it can flip to any
* side when there isn't room for its preferred one), so it stays correct
* without any extra wiring at the call site.
*
* Border and shadow both come from `.popover-surface` (globals.css) —
* stacked `drop-shadow` filters instead of a CSS `border` + `box-shadow` —
* because those are confined to the content box's own rectangle and cut
* across the arrow's base instead of wrapping around it. Filters trace the
* rendered alpha silhouette of the whole element (box + arrow together), so
* both the border-like outline and the shadow stay continuous across the
* seam. `drop-shadow()` only takes offset-x/offset-y/blur — no `box-shadow`
* style spread — so the border-like layer uses a hair of blur (`0 0 1px`)
* instead of a crisp spread ring. `PopoverArrow` itself only needs a fill
* color, no stroke.
*/
export const Popover = PopoverPrimitive.Root;
export const PopoverTrigger = PopoverPrimitive.Trigger;
export const PopoverAnchor = PopoverPrimitive.Anchor;
export const PopoverArrow = React.forwardRef<
React.ComponentRef<typeof PopoverPrimitive.Arrow>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Arrow>
>(({ className, width = 12, height = 6, ...props }, ref) => (
<PopoverPrimitive.Arrow
ref={ref}
width={width}
height={height}
className={cn("fill-background", className)}
{...props}
/>
));
PopoverArrow.displayName = PopoverPrimitive.Arrow.displayName;
export const PopoverContent = React.forwardRef<
React.ComponentRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(
(
{ className, align = "center", sideOffset = 8, children, ...props },
ref,
) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"bg-background text-foreground animate-popover-content popover-surface z-50 w-72 rounded-md p-4 outline-none",
className,
)}
{...props}
>
{children}
<PopoverArrow />
</PopoverPrimitive.Content>
</PopoverPrimitive.Portal>
),
);
PopoverContent.displayName = PopoverPrimitive.Content.displayName;