Skip to content

Popover ​

A floating card triggered by hover、click、focus or contextmenu. Built on top of Tooltip.

Basic Usage ​

Use placement to set position of Popover, and use content to set content of the Popover.

vue
<template>
  <div class="max-w-600px mx-auto">
    <div class="flex justify-center">
      <re-popover placement="top-start" content="Top Start Text Prompt" title="Title">
        <re-button>top-start</re-button>
      </re-popover>
      <re-popover placement="top" content="Top Center Text Prompt" title="Title">
        <re-button>top</re-button>
      </re-popover>
      <re-popover placement="top-end" content="Top End Text Prompt" title="Title">
        <re-button>top-end</re-button>
      </re-popover>
    </div>
    <div class="flex justify-between">
      <div class="flex flex-col items-start">
        <re-popover
          placement="left-start"
          width="120"
          content="Left Start Text Prompt"
          title="Title"
        >
          <re-button>left-start</re-button>
        </re-popover>
        <re-popover placement="left" width="120px" content="Left Middle Text Prompt" title="Title">
          <re-button class="mt-1em ml-0!">left</re-button>
        </re-popover>
        <re-popover placement="left-end" width="120px" content="Left End Text Prompt" title="Title">
          <re-button class="mt-1em ml-0!">left-end</re-button>
        </re-popover>
      </div>
      <div class="flex flex-col items-end">
        <re-popover
          placement="right-start"
          width="120px"
          content="Right Start Text Prompt"
          title="Title"
        >
          <re-button>right-start</re-button>
        </re-popover>
        <re-popover
          placement="right"
          width="120px"
          content="Right Center Text Prompt"
          title="Title"
        >
          <re-button class="mt-1em ml-0!">right</re-button>
        </re-popover>
        <re-popover
          placement="right-end"
          width="120px"
          content="Right End Text Prompt"
          title="Title"
        >
          <re-button class="mt-1em ml-0!">right-end</re-button>
        </re-popover>
      </div>
    </div>
    <div class="flex justify-center">
      <re-popover placement="bottom-start" content="Bottom Start Text Prompt" title="Title">
        <re-button>bottom-start</re-button>
      </re-popover>
      <re-popover placement="bottom" content="Bottom Center Text Prompt" title="Title">
        <re-button>bottom</re-button>
      </re-popover>
      <re-popover placement="bottom-end" content="Bottom End Text Prompt" title="Title">
        <re-button>bottom-end</re-button>
      </re-popover>
    </div>
  </div>
</template>

Title ​

Use title or slot#title to provide the title.

vue
<template>
  <re-popover
    content="Content text Content text Content text Content"
    title="This is a Title"
    max-width="200"
    trigger="hover"
  >
    <re-button>Hover me</re-button>
  </re-popover>
  <re-popover
    content="Content text Content text Content text Content"
    max-width="200"
    trigger="hover"
  >
    <template #title>
      <div style="color: var(--re-color-primary-base); padding: 0.5em 1em; font-size: 16px">
        This is a Slot Title
      </div>
    </template>
    <re-button>Slot Title</re-button>
  </re-popover>
</template>

Trigger ​

Use hover 、 click 、 focus and contextmenu to display Popover by trigger attribute.

vue
<template>
  <re-popover
    content="Popover text Popover text Popover text"
    width="200"
    title="Title"
    trigger="hover"
  >
    <re-button>Hover</re-button>
  </re-popover>
  <re-popover
    content="Popover text Popover text Popover text"
    width="200"
    title="Title"
    trigger="click"
  >
    <re-button>Click</re-button>
  </re-popover>
  <re-popover
    content="Popover text Popover text Popover text"
    width="200"
    title="Title"
    trigger="focus"
  >
    <re-button>Focus</re-button>
  </re-popover>
  <re-popover
    content="Popover text Popover text Popover text"
    width="200"
    title="Title"
    trigger="contextmenu"
    placement="right"
  >
    <re-button>Contextmenu</re-button>
  </re-popover>
</template>

Content ​

Use content or slot#content to set Popover content, is-html for render HTML content.

vue
<template>
  <re-popover content="<i>Text Content</i>" title="Title" trigger="hover">
    <re-button>Text Content</re-button>
  </re-popover>
  <re-popover content="<i>HTML Content</i>" is-html title="Title" trigger="hover">
    <re-button>HTML Content</re-button>
  </re-popover>
  <re-popover title="Title" trigger="hover">
    <re-button>Slot Content</re-button>
    <template #content>
      <div style="color: var(--re-color-primary-base); padding: 0 1em 1em">The Slot Content</div>
    </template>
  </re-popover>
</template>

Disabled ​

Use disabled to disable Popover.

vue
<template>
  <div>
    <re-popover
      :disabled="disabled"
      content="Disabled Popover Disabled Popover Disabled Popover"
      width="200"
      title="Title"
      trigger="hover"
    >
      <re-button type="success" :disabled="disabled">
        {{ disabled ? 'Disabled' : 'Enabled' }}
      </re-button>
    </re-popover>

    <re-button class="ml-5em!" @click="toggle">Click to toggle</re-button>
  </div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'

const disabled = ref<boolean>(false)

const toggle = () => {
  disabled.value = !disabled.value
}
</script>

Customize Boundary ​

Use boundary and rootBoundary to set Popover boundary.

vue
<template>
  <div>
    <div class="flex flex-wrap">
      <div
        ref="container1"
        class="h-300px grow basis-300px of-x-hidden of-y-auto text-center demo-border"
      >
        <div class="h-300px"></div>
        <re-popover
          v-if="container1"
          :boundary="container1"
          content="Scroll me to top boundary"
          title="Title"
          width="200"
          :visible="visible"
        >
          <re-button>boundary: container</re-button>
        </re-popover>
        <div class="h-300px"></div>
      </div>
      <div
        ref="container2"
        class="h-300px grow flex basis-300px items-center of-x-auto demo-border"
      >
        <div class="w-500px shrink-0"></div>
        <re-popover
          v-if="container2"
          content="Scroll me to left boundary"
          title="Title"
          width="200"
          :boundary="container2"
          :visible="visible"
          placement="left"
        >
          <re-button>boundary: container</re-button>
        </re-popover>
        <div class="w-500px shrink-0"></div>
      </div>
    </div>
    <div
      ref="container3"
      class="h-300px flex-1 of-hidden of-y-auto shrink-0 text-center demo-border"
    >
      <div class="h-300px"></div>
      <re-popover
        content="Scroll me to body top boundary"
        title="Title"
        width="200"
        :visible="visible"
      >
        <re-button>rootBoundary: 'viewport'</re-button>
      </re-popover>
      <div class="h-300px"></div>
    </div>
  </div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'

const visible = ref<boolean>(true)

const container1 = ref()
const container2 = ref()
const container3 = ref()

onMounted(() => {
  container1.value.scrollTo(0, 160)
  container2.value.scrollTo(220, 0)
  container3.value.scrollTo(0, 160)
})
</script>

Flip ​

By default, the Popover layer will auto flip based on the viewport, but when one of the below:
1、boundary: 'clippingAncestors' and altBoundary: true.
2、elementContext: 'reference'.
it will flip based on the parent container of the reference which is overflow: hidden/auto/scroll.
Otherwise, use flipOptions attribute for more behaviors.

vue
<template>
  <div class="flex flex-wrap">
    <div
      ref="container1"
      class="h-300px grow basis-300px of-hidden of-y-auto shrink-0 text-center demo-border"
    >
      <div class="h-300px"></div>
      <re-popover
        v-model:visible="visible"
        alt-boundary
        content="Scroll me to top boundary"
        title="Title"
        width="200"
      >
        <re-button>altBoundary: true</re-button>
      </re-popover>
      <div class="h-300px"></div>
    </div>
    <div
      ref="container2"
      class="h-300px grow basis-300px flex items-center min-w-0 of-x-auto demo-border"
    >
      <div class="w-500px shrink-0"></div>
      <re-popover
        content="Scroll me to left boundary"
        alt-boundary
        :visible="visible"
        placement="left"
        title="Title"
        width="200"
      >
        <re-button>altBoundary: true</re-button>
      </re-popover>
      <div class="w-500px shrink-0"></div>
    </div>
  </div>
  <div class="flex flex-wrap">
    <div
      ref="container3"
      class="h-300px grow basis-300px of-hidden of-y-auto shrink-0 text-center demo-border"
    >
      <div class="h-300px"></div>
      <re-popover
        element-context="reference"
        content="elementContext: 'reference'"
        title="Title"
        width="200"
        :visible="visible"
      >
        <re-button>Scroll me to top boundary</re-button>
      </re-popover>
      <div class="h-300px"></div>
    </div>
    <div
      ref="container4"
      class="h-300px grow basis-300px flex items-center min-w-0 of-x-auto demo-border"
    >
      <div class="w-500px shrink-0"></div>
      <re-popover
        content="elementContext: 'reference'"
        element-context="reference"
        :visible="visible"
        placement="left"
        title="Title"
        width="200"
      >
        <re-button>Scroll me to left boundary</re-button>
      </re-popover>
      <div class="w-500px shrink-0"></div>
    </div>
  </div>
  <div class="flex flex-wrap">
    <div
      ref="container5"
      class="h-300px grow basis-300px of-hidden of-y-auto shrink-0 text-center demo-border"
    >
      <div class="h-300px"></div>
      <re-popover
        alt-boundary
        :flip-options="{ fallbackPlacements: ['top', 'right', 'bottom'] }"
        content="Scroll me to top boundary"
        title="Title"
        width="200"
        :visible="visible"
      >
        <re-button class="mr-15em">altBoundary: true</re-button>
      </re-popover>
      <div class="h-300px"></div>
    </div>
    <div
      ref="container6"
      class="h-300px grow basis-300px flex items-center min-w-0 of-x-auto demo-border"
    >
      <div class="w-500px shrink-0"></div>
      <re-popover
        content="Scroll me to left boundary"
        alt-boundary
        :flip-options="{ fallbackPlacements: ['top', 'right', 'bottom'] }"
        :visible="visible"
        placement="left"
        title="Title"
        width="200"
      >
        <re-button>altBoundary: true</re-button>
      </re-popover>
      <div class="w-500px shrink-0"></div>
    </div>
  </div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'

const visible = ref<boolean>(true)
const container1 = ref()
const container2 = ref()
const container3 = ref()
const container4 = ref()
const container5 = ref()
const container6 = ref()

onMounted(() => {
  container1.value?.scrollTo(0, 160)
  container2.value?.scrollTo(220, 0)
  container3.value?.scrollTo(0, 160)
  container4.value?.scrollTo(220, 0)
  container5.value?.scrollTo(0, 160)
  container6.value?.scrollTo(220, 0)
})
</script>

External Reference ​

Use is-external to activate external-ref, this allows you to place the reference element outside the Popover.

Basic External Trigger Reference

Multiple External Trigger References for singleton

vue
<template>
  <p>Basic External Trigger Reference</p>
  <re-popover
    content="Popover text Popover text Popover text"
    trigger="hover"
    is-external
    :external-ref="externalRef"
    title="Title"
    width="200"
  ></re-popover>
  <re-button ref="buttonRef" @mouseenter="setExternal">Hover me</re-button>

  <p>Multiple External Trigger References for singleton</p>
  <re-popover
    content="Popover text Popover text Popover text"
    trigger="hover"
    title="Title"
    width="200"
    is-external
    :external-ref="hoverRef"
  ></re-popover>
  <re-button ref="buttonRef1" @mouseenter="setRef1()">Hover me 1</re-button>
  <re-button ref="buttonRef2" @mouseenter="setRef2()">Hover me 2</re-button>
  <re-button ref="buttonRef3" @mouseenter="setRef3()">Hover me 3</re-button>
  <p></p>
  <re-popover
    content="Popover text Popover text Popover text"
    trigger="click"
    title="Title"
    width="200"
    is-external
    :external-ref="clickRef"
  ></re-popover>
  <re-button ref="buttonRef4" @click="setRef4()">Click me 1</re-button>
  <re-button ref="buttonRef5" @click="setRef5()">Click me 2</re-button>
  <re-button ref="buttonRef6" @click="setRef6()">Click me 3</re-button>
</template>
<script lang="ts" setup>
import { ref } from 'vue'

const buttonRef = ref()
const externalRef = ref()

const setExternal = () => {
  externalRef.value = buttonRef.value
}

const buttonRef1 = ref()
const buttonRef2 = ref()
const buttonRef3 = ref()

const hoverRef = ref()

const setRef1 = () => {
  hoverRef.value = buttonRef1.value
}
const setRef2 = () => {
  hoverRef.value = buttonRef2.value
}
const setRef3 = () => {
  hoverRef.value = buttonRef3.value
}

const buttonRef4 = ref()
const buttonRef5 = ref()
const buttonRef6 = ref()

const clickRef = ref()

const setRef4 = () => {
  clickRef.value = buttonRef4.value
}
const setRef5 = () => {
  clickRef.value = buttonRef5.value
}
const setRef6 = () => {
  clickRef.value = buttonRef6.value
}
</script>

Virtual Reference ​

Use is-virtual to activate virtual-ref, it accepts an object with layout properties to build the reference, visible maybe is needed.

vue
<template>
  <re-popover
    content="Popover text Popover text Popover text"
    :visible="visible"
    is-virtual
    :virtual-ref="virtualRef"
    width="200"
    title="Title"
  ></re-popover>
  <re-button @click="visible = !visible">Click me</re-button>
</template>
<script lang="ts" setup>
import { ref, onMounted, onUnmounted } from 'vue'

const visible = ref<boolean>(false)

const position = ref<Record<string, number>>({
  width: 0,
  height: 0,
  x: 0,
  y: 0,
  top: 0,
  left: 0,
  right: 0,
  bottom: 0
})

const virtualRef = ref({
  getBoundingClientRect: () => position.value
})

const move = ({ clientX, clientY }: MouseEvent) => {
  position.value = {
    width: 0,
    height: 0,
    x: clientX,
    y: clientY,
    top: clientY,
    left: clientX,
    right: clientX,
    bottom: clientY
  }
}

onMounted(() => {
  document.addEventListener('mousemove', move)
})
onUnmounted(() => {
  document.removeEventListener('mousemove', move)
})
</script>

Selection Range Build A Virtual Reference ​

Use is-virtual to activate virtual-ref, it also allows a selection Range to build the reference.

Select the text to visible the Popover
vue
<template>
  <re-popover
    content="Popover text Popover text Popover text"
    :visible="visible"
    is-virtual
    :virtual-ref="rangeRef"
    width="200"
    title="Title"
  ></re-popover>
  <span ref="textRef">Select the text to visible the Popover</span>
</template>
<script lang="ts" setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
const visible = ref<boolean>(false)

const textRef = ref<HTMLSpanElement | null>(null)
const range = ref<Range | null>()

const rangeRef = {
  getBoundingClientRect: () => range.value?.getBoundingClientRect(),
  getClientRects: () => range.value?.getClientRects()
}

const mouseup = () => {
  range.value?.selectNode(textRef.value!)
  const selection = window.getSelection()

  if (!textRef.value || !selection || selection.rangeCount === 0) return

  const currentRange = selection.getRangeAt(0)
  const container = currentRange.commonAncestorContainer
  const text = selection.toString().trim()

  // Ensure the selection originates from the target element
  if (textRef.value.contains(container!)) {
    range.value = currentRange.cloneRange()
    visible.value = !!text
  }
}

const selectionChange = () => {
  const selection = window.getSelection()
  const text = selection?.toString()?.trim()

  if (!text) {
    visible.value = false
  }
}

onMounted(() => {
  document.addEventListener('mouseup', mouseup)
  document.addEventListener('touchend', mouseup)
  document.addEventListener('selectionchange', selectionChange)
})

onBeforeUnmount(() => {
  document.removeEventListener('mouseup', mouseup)
  document.removeEventListener('touchend', mouseup)
  document.removeEventListener('selectionchange', selectionChange)
})
</script>

Popover Attributes ​

NameDescriptionTypeDefault
placementPopover positionenumtop
triggerThe trigger event nameenumhover
titlePopover titlestring''
contentPopover contentstring''
is-htmlWhether content is treated as HTML stringboolean—
disabledWhether Popover is disabledboolean—
visible / v-model:visibleWhether Popover is visibleboolean—
offsetOffset from the reference edge. See Floating UI OffsetOptions for detailsnumber | interface10
widthPopover widthnumber | string—
heightPopover heightnumber | string—
max-widthPopover max widthnumber | string—
max-heightPopover max heightnumber | string—
themePopover theme'dark' |'light'light
reverse-themewhether to reverse Popover theme colorbooleanfalse
append-toTarget element for appending PopovercssSelector | HTMLElementbody
teleportedWhether to teleport Popover to the appendTo targetbooleantrue
persistentWhether to keep the floating open on hoverbooleantrue
click-to-toggleWhether to toggle the floating on click, only trigger='click' booleantrue
floating-classCustom class for the floatingstring | object | Array—
floating-styleCustom style for the floatingstring | object | Array—
show-delayDelay(ms) before Popover appears , but not effective when visible is present or is-virtual or is-externalnumber0
hide-delayDelay(ms) before Popover disappears , but not effective when visible is present or is-virtual or is-externalnumber200
show-arrowWhether to show arrow for Popoverbooleantrue
arrow-sizeThe arrow sizenumber8
arrow-offsetPadding between the arrow and the Popover edgenumber4
arrow-optionsFloating UI ArrowOptions Parametersinterface—
transitionName of Vue3 Transition component for customize transitionstringpopover
css-transitionCss of Vue3 Transition componentbooleantrue
transformWhether to use transform positioning instead of top/left positioningbooleantrue
strategyThe type of CSS position property to use'absolute' | 'fixed'absolute
css-durationDuration of Vue3 Transition component for fade in/fade outnumber0
boundaryBoundary of Floating UI DetectOverflowOptions : interfaceunionclippingAncestors
alt-boundaryAltBoundary of Floating UI DetectOverflowOptionsbooleanfalse
root-boundaryRootBoundary of Floating UI DetectOverflowOptionsunionviewport
element-contextElementContext of Floating UI DetectOverflowOptions'reference' | 'floating'floating
flip-offsetOffset from boundary before flipping, Padding of Floating UI DetectOverflowOptionsunion8
flip-optionsFloating UI FlipOptions Parametersinterface—
inline-optionsFloating UI InlineOptions Parametersinterface—
is-externalWhether external reference is enabledboolean—
external-refExternal reference targetHTMLElement—
is-virtualWhether virtual reference is enabledboolean—
virtual-refVirtual reference object for Floating UI VirtualElementinterface—

Popover Events ​

NameDescriptionParameters
before-enterVue3 <Transition> event hookFunction
enterVue3 <Transition> event hookFunction
after-enterVue3 <Transition> event hookFunction
before-leaveVue3 <Transition> event hookFunction
leaveVue3 <Transition> event hookFunction
after-leaveVue3 <Transition> event hookFunction
enter-cancelledVue3 <Transition> event hookFunction
leave-cancelledVue3 <Transition> event hookFunction

Popover Slots ​

NameDescription
defaultCustomize default reference
titleCustomize the title
contentCustomize default content

Popover Exposes ​

NameDescriptionType
updateManually updates the Popover positionFunction
showManually show the PopoverFunction
hideManually hide the PopoverFunction
triggerRefThe trigger refernce elementHTMLElement
floatingRefThe Popover layer elementHTMLElement
arrowRefThe Popover arrow elementHTMLElement

MIT License