Skip to content
RTL Support: Full
Accessibility: Full
Translations: Not Needed

Popover

A floating card popping up when hovering, clicking, or focusing on a trigger element.

Basic Usage

The trigger prop controls how the popover opens: hover, click, or focus.

html
<template>
  <HLPopover content-class="!p-2" trigger="hover">
    <template #trigger>
      <HLButton>Hover Me</HLButton>
    </template>
    <span>Hover popover content</span>
  </HLPopover>
</template>
<script setup>
  import { HLPopover, HLButton } from '@platform-ui/highrise'
</script>
html
<template>
  <HLPopover content-class="!p-2" trigger="click">
    <template #trigger>
      <HLButton>Click Me</HLButton>
    </template>
    <span>Click popover content</span>
  </HLPopover>
</template>
<script setup>
  import { HLPopover, HLButton } from '@platform-ui/highrise'
</script>
html
<template>
  <HLPopover content-class="!p-2" trigger="focus">
    <template #trigger>
      <HLButton>Focus Me</HLButton>
    </template>
    <span>Focus popover content</span>
  </HLPopover>
</template>
<script setup>
  import { HLPopover, HLButton } from '@platform-ui/highrise'
</script>

Placement

The placement prop sets which side of the trigger the popover appears on, and where it aligns along that side. It accepts twelve values — each of top, right, bottom, and left, optionally suffixed with -start or -end. Hover any button below to see its placement.

vue
<template>
  <HLPopover v-for="placement in placementOptions" :key="placement" content-class="!p-2" trigger="hover" :placement="placement">
    <template #trigger>
      <HLButton>{{ placement }}</HLButton>
    </template>
    <span>{{ placement }}</span>
  </HLPopover>
</template>

<script setup lang="ts">
import { HLPopover, HLButton } from '@platform-ui/highrise'

const placementOptions = [
  'top-start', 'top', 'top-end',
  'right-start', 'right', 'right-end',
  'bottom-start', 'bottom', 'bottom-end',
  'left-start', 'left', 'left-end',
] as const
</script>

INFO

When there isn't room for the chosen placement, the popover flips to the opposite side automatically. Set :flip="false" to keep it pinned to the requested placement even when it overflows.

Use the #header and #footer slots to add a titled header and footer around the main content.

Vue
html
<template>
  <HLPopover content-class="!p-2" header-class="!p-2" footer-class="!p-2" trigger="click">
    <template #trigger>
      <HLButton>With Header & Footer</HLButton>
    </template>
    <template #header> Header Text </template>
    <span>Main content</span>
    <template #footer> Footer Text </template>
  </HLPopover>
</template>
<script setup>
  import { HLPopover, HLButton } from '@platform-ui/highrise'
</script>

Timing

Three props tune hover behaviour, and all three apply only when trigger is hover:

  • delay — how long the pointer must rest on the trigger before the popover opens (default 100ms).
  • duration — how long after the pointer leaves before it closes (default 100ms).
  • keepAliveOnHover — keeps the popover open while the pointer moves onto the panel itself (default true). Turn it off and the popover closes as soon as you leave the trigger, which makes content inside it unreachable.

Hover each button below to compare the defaults against a slower, more deliberate configuration.

vue
<template>
  <!-- Defaults: 100ms each way -->
  <HLPopover content-class="!p-2" trigger="hover">
    <template #trigger>
      <HLButton>Default (100ms)</HLButton>
    </template>
    <span>Opens and closes almost immediately.</span>
  </HLPopover>

  <!-- Slower: less twitchy for triggers the pointer crosses often -->
  <HLPopover content-class="!p-2" trigger="hover" :delay="600" :duration="600">
    <template #trigger>
      <HLButton>Slow (600ms)</HLButton>
    </template>
    <span>Waits before opening, and lingers after you leave.</span>
  </HLPopover>

  <!-- Panel content becomes unreachable with the pointer -->
  <HLPopover content-class="!p-2" trigger="hover" :keep-alive-on-hover="false">
    <template #trigger>
      <HLButton>keepAliveOnHover: false</HLButton>
    </template>
    <span>Try to move your pointer onto this panel — it closes first.</span>
  </HLPopover>
</template>

<script setup lang="ts">
import { HLPopover, HLButton } from '@platform-ui/highrise'
</script>

INFO

Keep keepAliveOnHover enabled whenever the popover contains links, buttons, or text the user needs to select — with it off, the panel disappears before the pointer can reach it.

Width and Scrolling

width accepts a pixel number, or the string 'trigger' to match the trigger element's width — useful for dropdown-style panels that should line up with the control that opened them. Left unset, the panel sizes to its content.

For long content, scrollable caps the panel height and scrolls the overflow instead of letting it run off screen. Pair it with content-style to set the maximum height.

vue
<template>
  <!-- Fixed pixel width -->
  <HLPopover content-class="!p-2" trigger="click" :width="300">
    <template #trigger>
      <HLButton>Fixed 300px</HLButton>
    </template>
    <span>This panel is always 300px wide.</span>
  </HLPopover>
  <!-- Match the trigger's width -->
  <HLPopover content-class="!p-2" trigger="click" width="trigger">
    <template #trigger>
      <HLButton>Width follows this wide trigger</HLButton>
    </template>
    <span>This panel matches the trigger's width.</span>
  </HLPopover>
  <!-- Cap the height and scroll the overflow -->
  <HLPopover content-class="!p-2" trigger="click" :width="280" scrollable :content-style="{ maxHeight: '160px' }">
    <template #trigger>
      <HLButton>Scrollable</HLButton>
    </template>
    <span>{{ longText }}</span>
  </HLPopover>
</template>

<script setup lang="ts">
import { HLPopover, HLButton } from '@platform-ui/highrise'

const longText = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit…'
</script>

Mounting and Position

By default the popover teleports its panel to body, which keeps it above surrounding content but detaches it from the trigger's scroll container. If the trigger sits inside a scrollable area, scrolling leaves the panel behind at its original position. Point to at an element inside that scroll container — or pass false to keep the panel exactly where the component sits — and the two move together.

Open a popover, then scroll this box.

Bottom of the scroll area.

vue
<template>
  <div id="popover-scroll-area" style="position: relative; height: 220px; overflow: auto;">
    <!-- Default: teleported to body, so it detaches while scrolling -->
    <HLPopover content-class="!p-2" trigger="click" placement="bottom">
      <template #trigger>
        <HLButton>Teleports to body</HLButton>
      </template>
      <span>Scroll — this panel stays behind.</span>
    </HLPopover>

    <!-- Scoped to the scroll container, so trigger and panel move together -->
    <HLPopover content-class="!p-2" trigger="click" placement="bottom" to="#popover-scroll-area">
      <template #trigger>
        <HLButton variant="secondary">Scoped to this box</HLButton>
      </template>
      <span>Scroll — this panel follows.</span>
    </HLPopover>
  </div>
</template>

<script setup lang="ts">
import { HLPopover, HLButton } from '@platform-ui/highrise'
</script>

INFO

to accepts a CSS selector, an element, or false. The teleport target must exist when the popover mounts. Give it position: relative so the panel positions against it. This is the same fix documented for overlays inside a drawer or a modal.

Stacking Order

z-index sets the stacking order of the popover panel. Leave it unset — the panel teleports to body and already sits above ordinary page content. Raise it only when the popover must clear another high-z-index layer that shares that top-level stacking context, such as a fixed app header or a full-screen overlay.

Overlapping the Trigger

overlap pulls the panel over its trigger instead of sitting beside it, so the trigger is covered while the popover is open. Useful for inline editors, where the panel should replace the control it came from rather than crowd it.

vue
<template>
  <HLPopover content-class="!p-2" trigger="click" placement="bottom" overlap>
    <template #trigger>
      <HLButton>overlap</HLButton>
    </template>
    <span>Covers the trigger.</span>
  </HLPopover>
</template>

Positioning at Exact Coordinates

x and y place the panel at viewport pixel coordinates, ignoring the trigger's position entirely. The usual case is a context menu that opens wherever the user right-clicks. Pair them with trigger="manual" and drive show yourself.

WARNING

x and y must be set together. Supplying only one is silently ignored and the popover falls back to normal placement.

Right-click inside the dashed area below — the panel opens at the pointer. Click anywhere else to dismiss it.

Right-click anywhere in this area
vue
<template>
  <div @contextmenu.prevent="handleContextMenu">Right-click anywhere in this area</div>

  <!-- No #trigger slot: x/y position the panel, so there's nothing to anchor to -->
  <HLPopover content-class="!p-2" trigger="manual" :show="show" :x="x" :y="y" @clickoutside="show = false">
    <span>Opened at {{ x }}, {{ y }}</span>
  </HLPopover>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { HLPopover } from '@platform-ui/highrise'

const show = ref(false)
const x = ref(0)
const y = ref(0)

const handleContextMenu = (e: MouseEvent) => {
  x.value = e.clientX
  y.value = e.clientY
  show.value = true
}
</script>

Custom Styling

Every region of the popover takes a class and a style prop, so you can restyle it without reaching into internals:

  • Panel regionscontent-class / content-style, header-class / header-style, footer-class / footer-style.
  • Arrowarrow-class / arrow-style target the arrow itself; arrow-wrapper-class / arrow-wrapper-style target the element positioning it. Use the wrapper to nudge the arrow's offset, and the arrow for its colour or size.
  • arrow-point-to-center aims the arrow at the middle of the trigger rather than at the popover's edge — noticeable on a wide trigger with an -start or -end placement.
  • animated toggles the pop-in transition; set it to false when the popover should appear instantly.

The *-class props are the better choice when the styling is reusable and belongs in a stylesheet; the *-style props suit one-off tweaks.

vue
<template>
  <HLPopover
    trigger="click"
    placement="bottom"
    arrow-point-to-center
    :animated="false"
    content-class="demo-pop-content"
    header-class="demo-pop-header"
    footer-class="demo-pop-footer"
    :arrow-style="{ backgroundColor: 'var(--blue-600)' }"
    :arrow-wrapper-style="{ transform: 'translateX(8px)' }"
  >
    <template #trigger>
      <HLButton>Styled, no animation</HLButton>
    </template>
    <template #header>Styled header</template>
    <span>The arrow is blue, nudged 8px, and points at the trigger's centre.</span>
    <template #footer>Styled footer</template>
  </HLPopover>
  <!-- The same regions, styled inline instead of by class -->
  <HLPopover
    trigger="click"
    placement="bottom"
    :content-style="{ padding: '12px', maxWidth: '240px' }"
    :header-style="{ padding: '12px', fontWeight: '600' }"
    :footer-style="{ padding: '12px', color: 'var(--gray-500)' }"
    :arrow-style="{ backgroundColor: 'red'}"
  >
    <template #trigger>
      <HLButton variant="secondary">Inline styles</HLButton>
    </template>
    <template #header>Inline header</template>
    <span>Each region styled with its <code>*-style</code> prop instead of a class.</span>
    <template #footer>Inline footer</template>
  </HLPopover>
</template>

<script setup lang="ts">
import { HLPopover, HLButton } from '@platform-ui/highrise'
</script>

<style>
.demo-pop-content {
  padding: 12px;
  max-width: 260px;
  background-color: var(--blue-50);
}
.demo-pop-header {
  padding: 12px;
  font-weight: 600;
  background-color: var(--blue-600);
  color: var(--base-white);
}
.demo-pop-footer {
  padding: 12px;
  background-color: var(--blue-50);
  color: var(--gray-600);
}
</style>

INFO

The popover ships with no padding by default, which is why the other examples on this page pass content-class="!p-2". When you supply your own content-class or content-style, set the padding you want there instead.

Manual Control

Set trigger="manual" when the popover should open in response to your own logic rather than a pointer or focus gesture — after a save completes, during an onboarding step, or from a button elsewhere on the page. With manual, interacting with the trigger element does nothing; you decide when it opens.

There are two ways to drive it, and they are mutually exclusive:

  • Controlled — bind show and update it yourself. Listen to on-update:show to keep your state in sync when the popover closes for any other reason.
  • Uncontrolled — leave show unset and call the setShow(boolean) method through a template ref. Use defaultShow if it should start open.
vue
<template>
  <HLButton @click="show = !show">{{ show ? 'Hide' : 'Show' }} popover</HLButton>

  <!-- `show` decides visibility; @update:show keeps local state in sync -->
  <HLPopover content-class="!p-2" trigger="manual" :show="show" @update:show="show = $event">
    <template #trigger>
      <HLButton variant="secondary">Controlled target</HLButton>
    </template>
    <span>Driven by the <code>show</code> prop.</span>
  </HLPopover>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { HLPopover, HLButton } from '@platform-ui/highrise'

const show = ref(false)
</script>
vue
<template>
  <HLButton @click="popoverRef?.setShow(true)">Open via setShow()</HLButton>

  <!-- No `show` prop — the ref's setShow() drives it instead -->
  <HLPopover ref="popoverRef" content-class="!p-2" trigger="manual">
    <template #trigger>
      <HLButton variant="secondary">Uncontrolled target</HLButton>
    </template>
    <span>
      Opened with <code>setShow(true)</code>.
      <HLButton size="xs" @click="popoverRef?.setShow(false)">Close</HLButton>
    </span>
  </HLPopover>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { HLPopover, HLButton } from '@platform-ui/highrise'

const popoverRef = ref<InstanceType<typeof HLPopover> | null>(null)
</script>

INFO

Pick one approach. Once you bind show, that prop is the only thing that decides visibility — setShow() is ignored, and so is defaultShow. Reach for setShow() only on a popover with no show binding.

Repositioning and the underlying instance

If the trigger moves after the popover opens — a layout shift, a resize, content loading in behind it — call syncPosition() to realign the panel. For anything the two methods don't cover, getPopoverRef() returns the underlying popover instance.

vue
<script setup lang="ts">
import { ref } from 'vue'
import { HLPopover } from '@platform-ui/highrise'

const popoverRef = ref<InstanceType<typeof HLPopover> | null>(null)

const onContentLoaded = () => {
  // The trigger grew while the panel was open — realign it.
  popoverRef.value?.syncPosition()
}
</script>

Accessibility

  • Mark the trigger with aria-haspopup (menu, dialog, tooltip) and keep aria-expanded / aria-controls in sync with the floating panel.
  • Inside the panel, set the correct role plus aria-labelledby / aria-describedby for its heading and body copy.
  • If the popover is dismissible, provide a close control with an aria-label describing what will close.

Imports

ts
import { HLPopover } from '@platform-ui/highrise'

Props

NameTypeDefaultDescription
animatedbooleantrueUse animation when popping up.
arrow-point-to-centerbooleanfalseWhether the arrow points to center of the trigger element.
arrow-classstring | undefinedundefinedArrow class of the popover.
arrow-stylestring | Object | undefinedundefinedArrow style of the popover.
arrow-wrapper-classstring | undefinedundefinedArrow class of the popover wrapper.
arrow-wrapper-stylestring | Object | undefinedundefinedArrow style of the popover wrapper.
content-classstring | undefinedundefinedContent class of the popover.
content-stylestring | Object | undefinedundefinedContent style of the popover.
default-showbooleanfalseWhether the popover is open on first render, for uncontrolled use. Ignored once show is set.
delaynumber100Popover showing delay when trigger is hover.
disabledbooleanfalseWhether the popover can't be activated.
get-disabled() => booleanundefinedCalled before opening; return true to block it. Use for conditions evaluated at open time rather than a static disabled.
display-directive'if' | 'show''if'The conditionally render directive to show popover content. if means using v-if to render content, show means using v-show to render content.
durationnumber100Popover vanish delay when trigger is hover.
flipbooleantrueWhether to flip the popover when there is no space for current placement.
footer-classstring | undefinedundefinedFooter class of the popover.
footer-stylestring | Object | undefinedundefinedFooter style of the popover.
header-classstring | undefinedundefinedHeader class of the popover.
header-stylestring | Object | undefinedundefinedHeader style of the popover.
keep-alive-on-hoverbooleantrueWhether to keep popover shown when hover on popover itself with trigger="hover".
overlapbooleanfalseOverlap trigger element.
placement'top-start' | 'top' | 'top-end' | 'right-start' | 'right' | 'right-end' | 'bottom-start' | 'bottom' | 'bottom-end' | 'left-start' | 'left' | 'left-end' | 'top'Popover placement.
rawbooleanfalseWhether to use no default styles.
scrollablebooleanfalseWhether the popover's content is scrollable.
show-arrowbooleantrueWhether to show arrow if set.
showboolean | undefinedundefinedWhether to show popover.
tostring | HTMLElement | false'body'Container node of the popover content. false will keep it at trigger container.
trigger'hover' | 'click' | 'focus' | 'manual''hover'The popover trigger type.
widthnumber | 'trigger' | undefinedundefined'trigger' means popover's width will follow its trigger's width.
xnumber | undefinedundefinedThe CSS left pixel value when popover manually positioned (x, y need to be set together).
ynumber | undefinedundefinedThe CSS top pixel value when popover manually positioned (x, y need to be set together).
z-indexnumber | undefinedundefinedThe z-index of the popover.
on-clickoutside(e: MouseEvent) => voidundefinedCallback function triggered when clickoutside.
on-update:show(value: boolean) => voidundefinedCallback on show status changes.

Slots

NameParametersDescription
trigger()The trigger element
header()Header content
default()Main content
footer()Footer content

Methods

NameParametersDescription
setShow(show: boolean)Programmatically show/hide popover
syncPosition()Manually sync popover position
getPopoverRef()Returns the underlying popover instance, for cases the two methods above don't cover