Popover
popoveruiClick-triggered floating panel anchored to a trigger element. Supports optional localStorage persistence and configurable dismissal (click-outside, escape, manual). Built on reka-ui 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-vue@latest add https://uipkge.dev/r/vue/popover.json$npx shadcn-vue@latest add https://uipkge.dev/r/vue/popover.json$yarn dlx shadcn-vue@latest add https://uipkge.dev/r/vue/popover.json$bunx shadcn-vue@latest add https://uipkge.dev/r/vue/popover.jsonnpx shadcn-vue@latest add @uipkge/popoverInstalls to:app/components/ui/popover/Examples
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
persist | string | boolean | false | optional |
closeBehavior | PopoverCloseBehavior | 'auto' | optional |
Schema
Type aliases from this item's source — use them to shape the data you pass in.
PopoverContextinterface PopoverContext {
closeBehavior: Ref<PopoverCloseBehavior>
}npm dependencies
Used by
Files installed (6)
app/components/ui/popover/Popover.vue1.8 kB
<script setup lang="ts"> import type { PopoverRootEmits, PopoverRootProps } from 'reka-ui' import { PopoverRoot, useForwardPropsEmits } from 'reka-ui' import { computed, onMounted, provide, ref, toRef, useId, watch } from 'vue' import { POPOVER_INJECTION_KEY, type PopoverCloseBehavior } from './context' const props = withDefaults( defineProps< PopoverRootProps & { persist?: string | boolean closeBehavior?: PopoverCloseBehavior } >(), { persist: false, closeBehavior: 'auto', }, ) const emits = defineEmits<PopoverRootEmits>() provide(POPOVER_INJECTION_KEY, { closeBehavior: toRef(props, 'closeBehavior'), }) const autoId = useId() const storageKey = computed(() => { if (props.persist === false) return null if (props.persist === true) return `uipkge-popover-${autoId}` return props.persist }) const localOpen = ref<boolean>(props.defaultOpen ?? false) onMounted(() => { if (!storageKey.value || typeof localStorage === 'undefined') return if (localStorage.getItem(storageKey.value) === '1') { localOpen.value = true if (props.open === undefined) { emits('update:open', true) } } }) watch( () => (props.open === undefined ? localOpen.value : props.open), (v) => { if (!storageKey.value || typeof localStorage === 'undefined') return if (v) localStorage.setItem(storageKey.value, '1') else localStorage.removeItem(storageKey.value) }, ) const forwarded = useForwardPropsEmits( computed(() => ({ open: props.open, defaultOpen: props.defaultOpen, modal: props.modal, })), emits, ) function onUpdateOpen(value: boolean) { localOpen.value = value emits('update:open', value) } </script> <template> <PopoverRoot v-slot="slotProps" data-uipkge data-slot="popover" v-bind="forwarded" @update:open="onUpdateOpen"> <slot v-bind="slotProps" /> </PopoverRoot> </template>app/components/ui/popover/PopoverAnchor.vue0.3 kB
<script setup lang="ts"> import type { PopoverAnchorProps } from 'reka-ui' import { PopoverAnchor } from 'reka-ui' const props = defineProps<PopoverAnchorProps>() </script> <template> <PopoverAnchor data-uipkge data-slot="popover-anchor" v-bind="props"> <slot /> </PopoverAnchor> </template>app/components/ui/popover/PopoverContent.vue2.5 kB
<script setup lang="ts"> import type { PopoverContentEmits, PopoverContentProps } from 'reka-ui' import type { HTMLAttributes } from 'vue' import { reactiveOmit } from '@vueuse/core' import { PopoverContent, PopoverPortal, useForwardPropsEmits } from 'reka-ui' import { inject } from 'vue' import { cn } from '@/lib/utils' import { POPOVER_INJECTION_KEY } from './context' defineOptions({ inheritAttrs: false, }) const props = withDefaults(defineProps<PopoverContentProps & { class?: HTMLAttributes['class'] }>(), { align: 'center', sideOffset: 4, }) const emits = defineEmits<PopoverContentEmits>() const ctx = inject(POPOVER_INJECTION_KEY, null) const delegatedProps = reactiveOmit(props, 'class') const forwarded = useForwardPropsEmits(delegatedProps, emits) // Compose so closeBehavior still applies when consumers pass their own handlers. // Spread after `forwarded` so we replace (not merge-double) the emit wrappers. function onPointerDownOutside(e: Event) { const mode = ctx?.closeBehavior.value ?? 'auto' if (mode === 'esc' || mode === 'manual' || mode === 'none') { e.preventDefault() } emits('pointerDownOutside', e as never) } function onEscapeKeyDown(e: KeyboardEvent) { const mode = ctx?.closeBehavior.value ?? 'auto' if (mode === 'click-outside' || mode === 'manual' || mode === 'none') { e.preventDefault() } emits('escapeKeyDown', e) } </script> <template> <PopoverPortal> <PopoverContent data-uipkge data-slot="popover-content" v-bind="{ ...$attrs, ...forwarded, onPointerDownOutside, onEscapeKeyDown, }" :class=" 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-(--reka-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', props.class, ) " > <slot /> </PopoverContent> </PopoverPortal> </template>app/components/ui/popover/PopoverTrigger.vue0.3 kB
<script setup lang="ts"> import type { PopoverTriggerProps } from 'reka-ui' import { PopoverTrigger } from 'reka-ui' const props = defineProps<PopoverTriggerProps>() </script> <template> <PopoverTrigger data-uipkge data-slot="popover-trigger" v-bind="props"> <slot /> </PopoverTrigger> </template>app/components/ui/popover/context.ts0.3 kB
import type { InjectionKey, Ref } from 'vue' export type PopoverCloseBehavior = 'auto' | 'click-outside' | 'esc' | 'manual' | 'none' export interface PopoverContext { closeBehavior: Ref<PopoverCloseBehavior> } export const POPOVER_INJECTION_KEY: InjectionKey<PopoverContext> = Symbol('uipkge-popover')app/components/ui/popover/index.ts0.2 kB
export { default as Popover } from './Popover.vue' export { default as PopoverAnchor } from './PopoverAnchor.vue' export { default as PopoverContent } from './PopoverContent.vue' export { default as PopoverTrigger } from './PopoverTrigger.vue'
Raw manifest:https://uipkge.dev/r/vue/popover.json