PXD

Popover

A pop-up box with no style, used to show some information.

Default

<script setup>
const content = 'The hymn of humanity is the hymn of courage.'
</script>

<template>
  <PStack justify="center" class="w-lg">
    <PPopover
      content-class="p-4 bg-background-100 shadow-sm border rounded-md"
      position="top-start"
    >
      <PButton> Top start </PButton>

      <template #content>
        {{ content }}
      </template>
    </PPopover>

    <PPopover content-class="p-4 bg-background-100 shadow-sm border rounded-md" position="top">
      <PButton> Top </PButton>

      <template #content>
        {{ content }}
      </template>
    </PPopover>

    <PPopover content-class="p-4 bg-background-100 shadow-sm border rounded-md" position="top-end">
      <PButton> Top end </PButton>

      <template #content>
        {{ content }}
      </template>
    </PPopover>
  </PStack>

  <PStack justify="between" class="w-lg my-2">
    <PStack direction="vertical">
      <PPopover
        content-class="p-4 bg-background-100 shadow-sm border rounded-md"
        position="left-start"
      >
        <PButton> Left start </PButton>

        <template #content>
          {{ content }}
        </template>
      </PPopover>

      <PPopover content-class="p-4 bg-background-100 shadow-sm border rounded-md" position="left">
        <PButton> Left </PButton>

        <template #content>
          {{ content }}
        </template>
      </PPopover>

      <PPopover
        content-class="p-4 bg-background-100 shadow-sm border rounded-md"
        position="left-end"
      >
        <PButton> Left end </PButton>

        <template #content>
          {{ content }}
        </template>
      </PPopover>
    </PStack>

    <PStack direction="vertical" align="end">
      <PPopover
        content-class="p-4 bg-background-100 shadow-sm border rounded-md"
        position="right-start"
      >
        <PButton> Right start </PButton>

        <template #content>
          {{ content }}
        </template>
      </PPopover>

      <PPopover content-class="p-4 bg-background-100 shadow-sm border rounded-md" position="right">
        <PButton> Right </PButton>

        <template #content>
          {{ content }}
        </template>
      </PPopover>

      <PPopover
        content-class="p-4 bg-background-100 shadow-sm border rounded-md"
        position="right-end"
      >
        <PButton> Right end </PButton>

        <template #content>
          {{ content }}
        </template>
      </PPopover>
    </PStack>
  </PStack>

  <PStack justify="center" class="w-lg">
    <PPopover
      content-class="p-4 bg-background-100 shadow-sm border rounded-md"
      position="bottom-start"
    >
      <PButton> Bottom start </PButton>

      <template #content>
        {{ content }}
      </template>
    </PPopover>

    <PPopover content-class="p-4 bg-background-100 shadow-sm border rounded-md" position="bottom">
      <PButton> Bottom </PButton>

      <template #content>
        {{ content }}
      </template>
    </PPopover>

    <PPopover
      content-class="p-4 bg-background-100 shadow-sm border rounded-md"
      position="bottom-end"
    >
      <PButton> Bottom end </PButton>

      <template #content>
        {{ content }}
      </template>
    </PPopover>
  </PStack>
</template>

Trigger Methods

<script setup>
import { ref } from 'vue'

const visible = ref(false)
const content =
  'The woods are lovely, dark and deep, but I have promises to keep, and miles to go before I sleep'
</script>

<template>
  <PStack>
    <PPopover content-class="p-4 bg-background-100 shadow-sm border rounded-md" trigger="hover">
      <PButton> Hover to active </PButton>

      <template #content>
        {{ content }}
      </template>
    </PPopover>

    <PPopover content-class="p-4 bg-background-100 shadow-sm border rounded-md" trigger="click">
      <PButton> Click to active </PButton>

      <template #content>
        {{ content }}
      </template>
    </PPopover>

    <PPopover
      content-class="p-4 bg-background-100 shadow-sm border rounded-md"
      trigger="contextmenu"
    >
      <PButton> Contextmenu to active </PButton>

      <template #content>
        {{ content }}
      </template>
    </PPopover>

    <PPopover
      content-class="p-4 bg-background-100 shadow-sm border rounded-md"
      :trigger="['hover', 'click']"
    >
      <PButton> Hover/Click to active </PButton>

      <template #content>
        {{ content }}
      </template>
    </PPopover>

    <PPopover
      v-model="visible"
      trigger="manual"
      content-class="p-4 bg-background-100 shadow-sm border rounded-md"
    >
      <PButton @click="visible = !visible"> Manual to active </PButton>

      <template #content>
        {{ content }}
      </template>
    </PPopover>
  </PStack>
</template>

Multiple trigger elements

Use trigger-selector when several DOM elements should share one popover instance. The popover keeps the same floating element open and updates its position to the active matched trigger.

<script setup>
const actions = [
  { key: 'bold', label: 'B', description: 'Make selected text bold.' },
  { key: 'italic', label: 'I', description: 'Make selected text italic.' },
  { key: 'underline', label: 'U', description: 'Underline selected text.' },
]
</script>

<template>
  <PPopover
    trigger="hover"
    trigger-selector="[data-popover-trigger]"
    content-class="p-3 bg-background-100 shadow-sm border rounded-md text-sm"
  >
    <PStack>
      <button
        v-for="action in actions"
        :key="action.key"
        type="button"
        data-popover-trigger
        :data-popover-key="action.key"
        :data-popover-description="action.description"
        class="h-8 w-8 rounded-md border bg-background-100 font-medium"
      >
        {{ action.label }}
      </button>
    </PStack>

    <template #content="{ activeTrigger, activeTriggerIndex }">
      {{ activeTrigger.dataset.popoverDescription + activeTriggerIndex }}
    </template>
  </PPopover>
</template>

trigger-selector matches the final DOM elements inside the default slot. When using it on a Vue component, make sure the component forwards the matching attribute or class to a real DOM element. The activeTrigger slot prop is the matched DOM element, not the Vue component instance.

Align to point

Set align-point to position the Popover at the pointer position. With hover, the Popover follows the pointer while it is inside the trigger. With click, it toggles at the clicked point. With contextmenu, each right-click updates the position and a click hides the Popover.

Move, click, or right-click in this area
<script setup>
import { ref } from 'vue'

const content = 'The Popover follows the pointer position.'
const trigger = ref('hover')

const toggleButtonOptions = [
  { label: 'Hover', value: 'hover' },
  { label: 'Click', value: 'click' },
  { label: 'Context Menu', value: 'contextmenu' },
]
</script>

<template>
  <PStack direction="vertical" align="start" class="w-full gap-3">
    <PToggleButtonGroup v-model="trigger" size="sm" variant="outline" :multiple="false" :options="toggleButtonOptions" />

    <PPopover
      align-point
      :trigger="trigger"
      class="w-full"
      :interactive="false"
      content-class="p-3 bg-background-100 shadow-sm border rounded-md text-sm"
    >
      <div class="flex h-48 w-full items-center justify-center rounded-md border border-dashed text-sm">
        Move, click, or right-click in this area
      </div>

      <template #content>
        {{ content }}
      </template>
    </PPopover>
  </PStack>
</template>

Offset

<script setup>
const content =
  'Two roads diverged in a wood, and I — I took the one less traveled by, and that has made all the difference.'
</script>

<template>
  <PPopover content-class="p-4 bg-background-100 shadow-sm border rounded-md" :offset="30">
    <PButton> Hover to active </PButton>

    <template #content>
      {{ content }}
    </template>
  </PPopover>
</template>

Max width

<script setup>
const content = 'Do not go gentle into that good night, rage, rage against the dying of the light.'
</script>

<template>
  <PPopover content-class="p-4 bg-background-100 shadow-sm border rounded-md" :max-width="200">
    <PButton> Hover to active </PButton>

    <template #content>
      {{ content }}
    </template>
  </PPopover>
</template>

Fill trigger width

<script setup>
const content = 'Do not go gentle into that good night, rage, rage against the dying of the light.'
</script>

<template>
  <PPopover
    :fill-trigger-width="false"
    content-class="p-4 bg-background-100 shadow-sm border rounded-md"
    :max-width="200"
  >
    <PButton> Hover to active </PButton>

    <template #content>
      {{ content }}
    </template>
  </PPopover>
</template>

closeOnPressEscape

<script setup>
const content = 'Do not go gentle into that good night, rage, rage against the dying of the light.'
</script>

<template>
  <PPopover close-on-press-escape content-class="p-4 bg-background-100 shadow-sm border rounded-md" :max-width="200">
    <PButton> Hover to active </PButton>

    <template #content>
      {{ content }}
    </template>
  </PPopover>
</template>

Props

NameTypeDefaultDescription
z-indexnumber | string-Custom z-index for the Popover wrapper
offsetnumber-Gap in px between the Popover and its trigger
trigger'click' | 'hover' | 'contextmenu' | 'manual' | ('click' | 'hover' | 'contextmenu' | 'manual')[]() => ['hover']Trigger methods; takes one or an array of hover, click, contextmenu, manual
trigger-selectorstring-Selector for multiple DOM triggers inside the default slot.
align-pointboolean-Align the Popover to the pointer position.
disabledboolean-Ignore every trigger event so the Popover cannot be shown
adaptiveboolean-Render as a fullscreen overlay with dimmed backdrop, locked scroll and slide motion
max-widthnumber | string-Max width of the content; a number is treated as px
fill-trigger-widthbooleantrueMatch the Popover min width to the trigger width.
position'top' | 'right' | 'bottom' | 'left' | ...bottomPreferred placement: top, right, bottom or left, each with optional -start / -end
show-delaynumber0Delay in ms before the Popover is shown
hide-delaynumber0Delay in ms before the Popover is hidden
destroy-delaynumber3000Delay before unmounting content after hide.
show-arrowboolean-Render the arrow pointing back to the trigger
arrow-colorstring-Fill color of the arrow, any CSS color value
model-valueboolean-Controlled visibility, normally used with the manual trigger
interactivebooleantrueKeep the Popover open while the pointer moves over it
auto-positionbooleantrueReposition on scroll and resize, and flip when it would be clipped
wrapper-classstring | any[] | object-Class bound to the Popover wrapper
content-classstring | any[] | object-Class bound to the Popover content
content-styleCSSProperties | string-Inline style bound to the Popover content
toggle-on-triggerbooleantrueHide the Popover when the trigger is activated again
auto-focus-elementstring | booleanfalseFocus the first tabbable element on open, or the one matching a selector
return-focus-on-deactivatebooleantrueReturn focus to the trigger when the Popover closes
close-on-invisiblebooleantrueHide the Popover when the trigger is clipped or scrolled out of view
close-on-press-escapebooleantrueClose the Popover when pressing Escape
lock-scroll-on-visibleboolean-Currently unused: the overlay is bound to adaptive instead, so scroll locks only in adaptive mode

Events

NameTypeDescription
show() => voidEmitted when the popover becomes visible.
hide() => voidEmitted when the popover becomes hidden.
escape(event: KeyboardEvent) => voidEmitted when Escape is pressed while the popover is open, before it hides.
outside-click(event: PointerEvent) => voidEmitted when a click lands outside both the trigger and the popover while it is open.
trigger-click(event: PointerEvent) => voidEmitted when one of the resolved trigger elements is clicked.
visible-change(visible: boolean) => voidEmitted when the visible state changes.
wrapper-keydown(event: KeyboardEvent) => voidEmitted when a key is pressed inside the popover while it is open.
update:modelValue(visible: boolean) => voidEmitted right after show or hide with the new visibility.

Slots

NameDescription
defaultTrigger content
contentPopover content. Slot props: activeTrigger: HTMLElement | null, activeTriggerIndex: number.

Methods

NameTypeDescription
show() => Promise<void>Show the popover after the configured show-delay.
hide(immediate?: boolean) => Promise<void>Hide the popover, skipping the hide-delay when immediate is true.
update() => voidRecompute the popover position on the next animation frame.

Source

Source