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.
<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><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><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.
<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.
With Header and Footer
Use the #header and #footer slots to add a titled header and footer around the main content.
<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 (default100ms).duration— how long after the pointer leaves before it closes (default100ms).keepAliveOnHover— keeps the popover open while the pointer moves onto the panel itself (defaulttrue). 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.
<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.
<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.
<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.
<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.
<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 regions —
content-class/content-style,header-class/header-style,footer-class/footer-style. - Arrow —
arrow-class/arrow-styletarget the arrow itself;arrow-wrapper-class/arrow-wrapper-styletarget the element positioning it. Use the wrapper to nudge the arrow's offset, and the arrow for its colour or size. arrow-point-to-centeraims the arrow at the middle of the trigger rather than at the popover's edge — noticeable on a wide trigger with an-startor-endplacement.animatedtoggles the pop-in transition; set it tofalsewhen 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.
<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
showand update it yourself. Listen toon-update:showto keep your state in sync when the popover closes for any other reason. - Uncontrolled — leave
showunset and call thesetShow(boolean)method through a template ref. UsedefaultShowif it should start open.
<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><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.
<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 keeparia-expanded/aria-controlsin sync with the floating panel. - Inside the panel, set the correct
roleplusaria-labelledby/aria-describedbyfor its heading and body copy. - If the popover is dismissible, provide a close control with an
aria-labeldescribing what will close.
Imports
import { HLPopover } from '@platform-ui/highrise'Props
| Name | Type | Default | Description |
|---|---|---|---|
| animated | boolean | true | Use animation when popping up. |
| arrow-point-to-center | boolean | false | Whether the arrow points to center of the trigger element. |
| arrow-class | string | undefined | undefined | Arrow class of the popover. |
| arrow-style | string | Object | undefined | undefined | Arrow style of the popover. |
| arrow-wrapper-class | string | undefined | undefined | Arrow class of the popover wrapper. |
| arrow-wrapper-style | string | Object | undefined | undefined | Arrow style of the popover wrapper. |
| content-class | string | undefined | undefined | Content class of the popover. |
| content-style | string | Object | undefined | undefined | Content style of the popover. |
| default-show | boolean | false | Whether the popover is open on first render, for uncontrolled use. Ignored once show is set. |
| delay | number | 100 | Popover showing delay when trigger is hover. |
| disabled | boolean | false | Whether the popover can't be activated. |
| get-disabled | () => boolean | undefined | Called 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. |
| duration | number | 100 | Popover vanish delay when trigger is hover. |
| flip | boolean | true | Whether to flip the popover when there is no space for current placement. |
| footer-class | string | undefined | undefined | Footer class of the popover. |
| footer-style | string | Object | undefined | undefined | Footer style of the popover. |
| header-class | string | undefined | undefined | Header class of the popover. |
| header-style | string | Object | undefined | undefined | Header style of the popover. |
| keep-alive-on-hover | boolean | true | Whether to keep popover shown when hover on popover itself with trigger="hover". |
| overlap | boolean | false | Overlap 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. |
| raw | boolean | false | Whether to use no default styles. |
| scrollable | boolean | false | Whether the popover's content is scrollable. |
| show-arrow | boolean | true | Whether to show arrow if set. |
| show | boolean | undefined | undefined | Whether to show popover. |
| to | string | 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. |
| width | number | 'trigger' | undefined | undefined | 'trigger' means popover's width will follow its trigger's width. |
| x | number | undefined | undefined | The CSS left pixel value when popover manually positioned (x, y need to be set together). |
| y | number | undefined | undefined | The CSS top pixel value when popover manually positioned (x, y need to be set together). |
| z-index | number | undefined | undefined | The z-index of the popover. |
| on-clickoutside | (e: MouseEvent) => void | undefined | Callback function triggered when clickoutside. |
| on-update:show | (value: boolean) => void | undefined | Callback on show status changes. |
Slots
| Name | Parameters | Description |
|---|---|---|
| trigger | () | The trigger element |
| header | () | Header content |
| default | () | Main content |
| footer | () | Footer content |
Methods
| Name | Parameters | Description |
|---|---|---|
| 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 |