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.
_popoverAnchor-objectProps for <PopoverAnchor> component.
_popoverClose-objectProps for <PopoverClose> component.
_popoverArrow-objectProps for <PopoverArrow> component.
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.
una-objectAn object containing classes for styling various parts of the popover component.
Preview
Code

Close & Arrow

PropDefaultTypeDescription
showClosefalsebooleanWhether to show the close button.
arrowfalsebooleanWhether to show the arrow.
Preview
Code

Slots

NamePropsDescription
triggeropen, closeThe button trigger.
anchoropen, closeThe optional anchor for the popover.
defaultcloseThe 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-border bg-popover p-4 text-popover-foreground shadow-md outline-none 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',
  'popover-close': 'absolute right-2 top-2',
  'popover-arrow': '-mt-1px fill-border stroke-border',
}

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

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

Props

types/popover.ts
import type { PopoverAnchorProps, PopoverArrowProps, PopoverCloseProps, PopoverContentProps, PopoverRootProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import type { NButtonProps } from './button'

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

export interface NPopoverProps extends PopoverRootProps {
  /** Props for the popover content */
  _popoverContent?: NPopoverContentProps
  /** Props for the popover anchor */
  _popoverAnchor?: Partial<NPopoverAnchorProps>
  /** Props for the popover arrow */
  _popoverArrow?: Partial<NPopoverArrowProps>
  /** Props for the popover close button */
  _popoverClose?: Partial<NPopoverCloseProps>

  /**
   * Whether to show the close button.
   * @default false
   */
  showClose?: boolean
  /**
   * Whether to show the arrow.
   * @default false
   */
  arrow?: boolean

  /**
   * `UnaUI` preset configuration
   *
   * @see https://github.com/una-ui/una-ui/blob/main/packages/preset/src/_shortcuts/popover.ts
   */
  una?: NPopoverUnaProps
}

export interface NPopoverContentProps extends PopoverContentProps, BaseExtensions {
  una?: Pick<NPopoverUnaProps, 'popoverContent'>
}

export interface NPopoverAnchorProps extends PopoverAnchorProps, BaseExtensions {
}

export interface NPopoverArrowProps extends PopoverArrowProps, BaseExtensions {
  una?: Pick<NPopoverUnaProps, 'popoverArrow'>
}

export interface NPopoverCloseProps extends PopoverCloseProps, NButtonProps {
}

interface NPopoverUnaProps {
  popoverContent?: HTMLAttributes['class']
  popoverArrow?: HTMLAttributes['class']
}

Components

Popover.vue
PopoverContent.vue
<script setup lang="ts">
import type { PopoverRootEmits } from 'reka-ui'
import type { NPopoverProps } from '../../../types'
import { reactiveOmit } from '@vueuse/core'
import { PopoverAnchor, PopoverArrow, PopoverRoot, PopoverTrigger, useForwardPropsEmits } from 'reka-ui'
import { cn } from '../../../utils'
import NPopoverClose from './PopoverClose.vue'
import NPopoverContent from './PopoverContent.vue'

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

const delegatedProps = reactiveOmit(props, ['una', 'showClose', 'arrow', '_popoverContent', '_popoverAnchor', '_popoverArrow', '_popoverClose'])

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

<template>
  <PopoverRoot v-slot="{ open, close }" v-bind="forwarded">
    <PopoverTrigger v-if="$slots.trigger" as-child>
      <slot name="trigger" :open :close />
    </PopoverTrigger>
    <PopoverAnchor
      v-if="$slots.anchor || _popoverAnchor?.reference"
      data-slot="popover-anchor"
      v-bind="_popoverAnchor"
    >
      <slot name="anchor" :open :close />
    </PopoverAnchor>
    <NPopoverContent v-bind="_popoverContent" :una>
      <slot :close />
      <NPopoverClose
        v-if="showClose"
        data-slot="popover-close"
        v-bind="_popoverClose"
        class="popover-close"
      >
        <slot name="close" :close />
      </NPopoverClose>
      <PopoverArrow
        v-if="arrow"
        data-slot="popover-arrow"
        v-bind="_popoverArrow"
        :class="cn('popover-arrow', una?.popoverArrow)"
      />
    </NPopoverContent>
  </PopoverRoot>
</template>