Popover
popoveruiClick-triggered floating panel anchored to a trigger element. Supports optional localStorage persistence and configurable dismissal (click-outside, escape, manual). Built on @radix-ui/react-popover with collision detection.
When to use which
Decision GuideDialogs capture immediate user focus for critical actions. Sheets slide in from screen edges for deep secondary forms, while Popovers provide lightweight contextual cards.
| Component | Role | Use when |
|---|---|---|
| Dialog Critical confirmations, destructive warnings, or focused single-purpose tasks requiring user resolution. | Modal Window | |
| Sheet Complex edit forms, filter panes, or shopping carts that retain underlying page context. | Off-Canvas Slide-Over | |
| Drawer Mobile bottom sheets, touch-friendly sliding panels, or secondary inspection trays. | Edge Panel / Bottom Sheet | |
| Popovercurrent Small contextual dropdowns, color pickers, date pickers, or inline configuration flyouts. | Contextual Floating Card |
Installation
$pnpm dlx shadcn@latest add https://uipkge.dev/r/react/popover.json$npx shadcn@latest add https://uipkge.dev/r/react/popover.json$yarn dlx shadcn@latest add https://uipkge.dev/r/react/popover.json$bunx shadcn@latest add https://uipkge.dev/r/react/popover.jsonnpx shadcn@latest add @uipkge-react/popoverInstalls to:components/ui/popover/Examples
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
persista string uses that key verbatim, `false` (default) disables. | string | boolean | false | optional |
closeBehaviorControls how the popover may be dismissed. | PopoverCloseBehavior | auto | optional |
Schema
Type aliases from this item's source — use them to shape the data you pass in.
PopoverContextValueinterface PopoverContextValue {
closeBehavior: PopoverCloseBehavior
}npm dependencies
Used by
Files installed (2)
components/ui/popover/popover.tsx5.4 kB
'use client' import * as React from 'react' import * as PopoverPrimitive from '@radix-ui/react-popover' import { cn } from '@/lib/utils' export type PopoverCloseBehavior = 'auto' | 'click-outside' | 'esc' | 'manual' | 'none' interface PopoverContextValue { closeBehavior: PopoverCloseBehavior } const PopoverContext = React.createContext<PopoverContextValue>({ closeBehavior: 'auto' }) export interface PopoverProps extends React.ComponentProps<typeof PopoverPrimitive.Root> { /** Persist open state in localStorage. `true` uses an auto-generated key, * a string uses that key verbatim, `false` (default) disables. */ persist?: string | boolean /** Controls how the popover may be dismissed. */ closeBehavior?: PopoverCloseBehavior } function Popover({ persist = false, closeBehavior = 'auto', open, defaultOpen, onOpenChange, children, ...props }: PopoverProps) { const autoId = React.useId() const storageKey = React.useMemo(() => { if (persist === false) return null if (persist === true) return `uipkge-popover-${autoId}` return persist }, [persist, autoId]) const isControlled = open !== undefined const [localOpen, setLocalOpen] = React.useState<boolean>(defaultOpen ?? false) // Hydrate from localStorage on mount. React.useEffect(() => { if (!storageKey || typeof localStorage === 'undefined') return if (localStorage.getItem(storageKey) === '1') { setLocalOpen(true) if (!isControlled) onOpenChange?.(true) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [storageKey]) // Persist current open state. const effectiveOpen = isControlled ? open : localOpen React.useEffect(() => { if (!storageKey || typeof localStorage === 'undefined') return if (effectiveOpen) localStorage.setItem(storageKey, '1') else localStorage.removeItem(storageKey) }, [storageKey, effectiveOpen]) function handleOpenChange(value: boolean) { setLocalOpen(value) onOpenChange?.(value) } return ( <PopoverContext.Provider value={{ closeBehavior }}> <PopoverPrimitive.Root data-uipkge="" data-slot="popover" open={open} defaultOpen={defaultOpen} onOpenChange={handleOpenChange} {...props} > {children} </PopoverPrimitive.Root> </PopoverContext.Provider> ) } const PopoverTrigger = React.forwardRef< React.ElementRef<typeof PopoverPrimitive.Trigger>, React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Trigger> >((props, ref) => <PopoverPrimitive.Trigger ref={ref} data-uipkge="" data-slot="popover-trigger" {...props} />) PopoverTrigger.displayName = PopoverPrimitive.Trigger.displayName const PopoverAnchor = React.forwardRef< React.ElementRef<typeof PopoverPrimitive.Anchor>, React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Anchor> >((props, ref) => <PopoverPrimitive.Anchor ref={ref} data-uipkge="" data-slot="popover-anchor" {...props} />) PopoverAnchor.displayName = PopoverPrimitive.Anchor.displayName const PopoverContent = React.forwardRef< React.ElementRef<typeof PopoverPrimitive.Content>, React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> >(({ className, align = 'center', sideOffset = 4, onPointerDownOutside, onEscapeKeyDown, ...props }, ref) => { const { closeBehavior } = React.useContext(PopoverContext) // Compose so closeBehavior still applies when consumers pass their own handlers // (handlers listed after {...props} would otherwise be overwritten). function handlePointerDownOutside( e: Parameters< NonNullable<React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>['onPointerDownOutside']> >[0], ) { if (closeBehavior === 'esc' || closeBehavior === 'manual' || closeBehavior === 'none') { e.preventDefault() } onPointerDownOutside?.(e) } function handleEscapeKeyDown(e: KeyboardEvent) { if (closeBehavior === 'click-outside' || closeBehavior === 'manual' || closeBehavior === 'none') { e.preventDefault() } onEscapeKeyDown?.(e) } return ( <PopoverPrimitive.Portal> <PopoverPrimitive.Content ref={ref} data-uipkge="" data-slot="popover-content" align={align} sideOffset={sideOffset} onPointerDownOutside={handlePointerDownOutside} onEscapeKeyDown={handleEscapeKeyDown} className={cn( 'bg-popover text-popover-foreground motion-safe:data-[state=open]:animate-in motion-safe:data-[state=closed]:animate-out motion-safe:data-[state=open]:ease-emphasized motion-safe:data-[state=open]:blur-in-2 motion-safe:data-[state=closed]:blur-out-2 motion-safe:data-[state=closed]:fade-out-0 motion-safe:data-[state=open]:fade-in-0 motion-safe:data-[state=closed]:zoom-out-95 motion-safe:data-[state=open]:zoom-in-95 motion-safe:data-[side=bottom]:slide-in-from-top-2 motion-safe:data-[side=left]:slide-in-from-right-2 motion-safe:data-[side=right]:slide-in-from-left-2 motion-safe:data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden motion-safe:data-[state=closed]:duration-[var(--dur-exit)] motion-safe:data-[state=open]:duration-200', className, )} {...props} /> </PopoverPrimitive.Portal> ) }) PopoverContent.displayName = PopoverPrimitive.Content.displayName export { Popover, PopoverTrigger, PopoverAnchor, PopoverContent }components/ui/popover/index.ts0.1 kB
export { Popover, PopoverTrigger, PopoverAnchor, PopoverContent, type PopoverProps, type PopoverCloseBehavior, } from './popover'
Raw manifest:https://uipkge.dev/r/react/popover.json