Popover

Displays rich content in a portal, triggered by a button.

Examples

Basic

PropDefaultTypeDescription
defaultOpenfalsebooleanThe open state of the popover when it is initially rendered. Use when you do not need to control its open state.
modalfalsebooleanThe modality of the popover. When set to true, interaction with outside elements will be disabled and only popover content will be visible to screen readers.
openfalsebooleanThe controlled open state of the popover.
Preview
Code

Content

PropDefaultTypeDescription
_popoverContent-objectThe component that pops out when the popover is open.
OptionsDefaultTypeDescription
alignstartstart, center, endThe preferred alignment against the trigger. May change when collisions occur.
alignOffset-numberAn offset in pixels from the start or end alignment options.
avoidCollisionsfalsebooleanWhen true, overrides the side and align preferences to prevent collisions with boundary edges.
disableOutsidePointerEventsfalsebooleanWhen true, hover/focus/click interactions will be disabled on elements outside the DismissableLayer. Users will need to click twice on outside elements to interact with them: once to close the DismissableLayer, and again to trigger the element.
forceMountfalsebooleanUsed to force mounting when more control is needed. Useful when controlling animation with Vue animation libraries.
hideWhenDetachedfalsebooleanWhether to hide the content when the trigger becomes fully occluded.
prioritizePositionfalsebooleanForce content to be position within the viewport. Might overlap the reference element, which may not be desired.
sidetoptop, right, bottom, leftThe preferred side of the trigger to render against when open. Will be reversed when collisions occur and avoidCollisions is enabled.
sideOffset-numberThe distance in pixels from the trigger.
stickypartialpartial, alwaysThe sticky behavior on the align axis. partial will keep the content in the boundary as long as the trigger is at least partially in the boundary whilst "always" will keep the content in the boundary regardless.
trapFocusfalsebooleanWhether focus should be trapped within the MenuContent
updatePositionStrategy-always, optimizedStrategy to update the position of the floating element on every animation frame.
Preview
Code

Slots

NamePropsDescription
triggeropenThe button trigger.
default-The popover content.

Presets

shortcuts/popover.ts
type PopoverPrefix = 'popover'

export const staticPopover: Record<`${PopoverPrefix}-${string}`, string> = {
  'popover-content': 'z-50 w-72 rounded-md border border-base bg-popover p-4 text-popover shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
}

export const dynamicPopover: [RegExp, (params: RegExpExecArray) => string][] = [
  // dynamic preset
]

export const popover = [
  ...dynamicPopover,
  staticPopover,
]

Props

types/popover.ts
import type { PopoverContentProps, PopoverRootProps } from 'radix-vue'
import type { HTMLAttributes } from 'vue'

interface BaseExtensions { class?: HTMLAttributes['class'] }

export interface NPopoverProps extends PopoverRootProps {
  /** Props for the popover content */
  _popoverContent?: NPopoverContentProps
}

export interface NPopoverContentProps extends PopoverContentProps, BaseExtensions {
}

Components

Popover.vue
PopoverContent.vue
<script setup lang="ts">
import type { PopoverRootEmits } from 'radix-vue'
import type { NPopoverProps } from '../../../types'
import { PopoverRoot, PopoverTrigger, useForwardPropsEmits } from 'radix-vue'
import { computed } from 'vue'
import NPopoverContent from './PopoverContent.vue'

const props = defineProps<NPopoverProps>()
const emits = defineEmits<PopoverRootEmits>()

const delegatedProps = computed(() => {
  const { _popoverContent, ...delegated } = props

  return delegated
})

const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>

<template>
  <PopoverRoot v-slot="{ open }" v-bind="forwarded">
    <PopoverTrigger as-child>
      <slot name="trigger" :open />
    </PopoverTrigger>
    <NPopoverContent v-bind="_popoverContent">
      <slot />
    </NPopoverContent>
  </PopoverRoot>
</template>