Popover Plugin for Vue 3
A tooltip and popover plugin for Vue.js.
Vue.js 3.2+ required.
- Based on Floating UI for smart positioning
- Supports triggers and content that are colocated or remotely positioned
- Supports multiple triggers with shared content component
- Supports Vue teleport under the hood
- Focus management built in
Usage
Tooltips
Basic Tooltip
Tooltip
components can display a simple content
message.
<template>
<Tooptip content="This is a very important message">
<BasicButton>Hover Me</BasicButton>
</Tooptip>
</template>
Tooltip with options
Tooltips accept multiple props that override the default tooltip behavior.
<template>
<Tooltip
content="This is a very important message"
placement="bottom-start"
action="click"
>
<BaseButton>Click Me</BaseButton>
</Tooltip>
</template>
Popovers
Popovers are used to display dynamic, interactive content over your UI. They have different styling and behavior than tooltips, but can be further customized using props.
Popovers require a trigger element and custom content to display.
Popover Triggers
Triggers are defined by either a PopoverTrigger
component or a v-popover
directive.
The difference between these options is that while a PopoverTrigger
component may be linked with an external PopoverContent
component, it also provides a content
slot to conveniently display popover content inline.
Alternatively, using a directive requires the use of an external PopoverContent
component to display its popover content.
Single trigger
Consider the use of a single trigger that is colocated with its popover content.
A PopoverTrigger
may define its content in a slot, while the directive requires use of a separate PopoverContent
component, linked by a common name
.
<!--Component trigger-->
<template>
<PopoverTrigger>
<BaseButton>
<IconSettings class="mr-2 w-4 h-4" />Settings
</BaseButton>
<template #content>
<div class="rounded-md space-y-1">
<BaseButton transparent>Account Settings</BaseButton>
<BaseButton transparent>Support</BaseButton>
<BaseButton transparent>License</BaseButton>
<BaseButton transparent>Sign out</BaseButton>
</div>
</template>
</PopoverTrigger>
</template>
<!--Directive trigger-->
<template>
<BaseButton v-popover="{ name: 'menu' }">
<IconSettings class="mr-2 w-4 h-4" />Settings
</BaseButton>
<PopoverContent name="menu">
<div class="rounded-md space-y-1">
<BaseButton transparent>Account Settings</BaseButton>
<BaseButton transparent>Support</BaseButton>
<BaseButton transparent>License</BaseButton>
<BaseButton transparent>Sign out</BaseButton>
</div>
</PopoverContent>
</template>
The extra complexity introduced by having to use a separate component linked by a common name
likely makes the directive a less attractive option for this simple use case.
Multiple triggers
When the popover trigger and content are not colocated, or a single content section needs be shared between multiple triggers, a PopoverContent
component is required with a name
prop.
The same name
prop must be set on the PopoverTarget
components or v-popover
directives to link the triggers and content.
Common popover options may be set on PopoverContent
via props, but triggers may override these options with their own props.
<!--Component trigger-->
<script setup>
import { ref } from 'vue';
const menu = ref([
{ label: 'Products', opts: { data: 'products' } },
{ label: 'Resources', opts: { data: 'resources' } },
{ label: 'Pricing', opts: { data: 'pricing', placement: 'bottom-end' } },
{ label: 'Settings', opts: { data: 'settings', placement: 'bottom-end' } },
]);
</script>
<template>
<div class="flex gap-2">
<PopoverTrigger
v-for="{ label, opts } in menu"
:key="label"
v-bind="{ ...opts, name: 'shared_popover' }"
>
<BaseButton transparent>
{{ label }}
</BaseButton>
</PopoverTrigger>
</div>
<PopoverContent name="shared_popover" v-slot="{ data }">
<div class="space-y-1">
<template v-if="data === 'products'">
<BaseButton transparent>Product A</BaseButton>
<BaseButton transparent>Product B</BaseButton>
<BaseButton transparent>Product C</BaseButton>
</template>
<template v-else-if="data === 'resources'">
<BaseButton transparent>Resource A</BaseButton>
<BaseButton transparent>Resource B</BaseButton>
<BaseButton transparent>Resource C</BaseButton>
<BaseButton transparent>Resource D</BaseButton>
</template>
<template v-else-if="data === 'pricing'">
<BaseButton transparent>Pricing A</BaseButton>
<BaseButton transparent>Pricing B</BaseButton>
</template>
<template v-else-if="data === 'settings'">
<BaseButton transparent>Account Settings</BaseButton>
<BaseButton transparent>Support</BaseButton>
<BaseButton transparent>License</BaseButton>
<BaseButton transparent>Sign out</BaseButton>
</template>
</div>
</PopoverContent>
</template>
Notice how we assign specific data
for each trigger. This data
can be any value, and is passed through to the PopoverContent
slot when triggered in order to display separate user interfaces, depending on which trigger is active.
Open state
Use the open
slot prop to detect if the popover is open for a particular PopoverTrigger
.
<template>
<PopoverTrigger :transitions="['fade', 'slide']">
<template #default="{ open }">
<BaseButton transparent>
Settings
<IconChevronDown v-if="!open" class="ml-2 w-3.5 h-3.5" />
<IconChevronUp v-else class="ml-2 w-3.5 h-3.5" />
</BaseButton>
</template>
<template #content>
<div class="rounded-md space-y-1">
<BaseButton transparent>Account Settings</BaseButton>
<BaseButton transparent>Support</BaseButton>
<BaseButton transparent>License</BaseButton>
<BaseButton transparent>Sign out</BaseButton>
</div>
</template>
</PopoverTrigger>
</template>
Transitions
By default, when PopoverContent
is shared between multiple triggers, the content pane will transition between the trigger elements as the trigger actions occur.
To disable this behavior, the move
option can be omitted from the transitions
array.
Focus management
When a popover is shown with the click
action, focus is placed inside it, as it is a focus trap. A user cannot tab outside of the popover until it is closed by an outside click or the escape key. If the popover has focus when it is closed, focus is placed back on the trigger element.
Events
Overview
There are multiple ways to register for popover events.
Generally, there are 3 types of events. Each event includes a readonly copy of the popover's state in the payload.
Show: Sent when a popover is opened.
Hide: Sent when a popover is hidden.
Update: Sent when a popover's state is updated. Included in the payload is an updates
object with the updated state properties as keys mapped to their old and new values in an array.
{
direction: 'top',
alignment: 'right',
updates: {
direction: ['bottom', 'top'],
alignment: ['left', 'right'],
}
}
Events may be registered directly on components or globally via helper methods included with the plugin.
Component Events
Event listeners for all event types can be directly set on PopoverTrigger
and PopoverContent
components.
If a PopoverContent
component is shared across multiple triggers, and it is transitioning from one trigger to another, a separate hide
event is emitted for the old trigger and a show
event is emitted for the new trigger.
Global Events
If you don't have direct access to PopoverTrigger
or PopoverContent
components, events can be registered globally on the document via popovershow
, popoverhide
and popoverupdate
.
Using onPopoverEvent()
Import the onPopoverEvent
helper to easily register handlers for these events. This helper returns a cleanup function that can be called at a later time.
import { onPopoverEvent } from 'v-popover';
const off = onPopoverEvent('popovershow', (e) => {
console.log('Popover displayed for', e.detail.name);
});
off();
Since this callback will get called for every popover in your app, you can check for specific popovers within the callback, or pass an optional filter object as the third parameter.
import { onPopoverEvent } from 'v-popover';
onPopoverEvent('popovershow', (e) => {
if (e.detail.name === 'my_popover') {
console.log('Popover displayed for', e.detail.name);
}
});
onPopoverEvent('popovershow', (e) => {
console.log('Popover displayed for', e.detail.name);
}, {
name: 'my_popover',
id: 'my_popover_trigger',
});
onPopoverEvent('popoverupdate', (e) => {
console.log('Direction changed for popover', e.detail.name);
}, {
name: 'my_popover',
id: 'my_popover_trigger',
updates: ['direction'],
})
Using usePopoverEvent()
Within components, you can call usePopoverEvent
to automatically register the callback in onMounted
and unregister the callback in onUnmounted
.
Otherwise, this function behaves exactly the same as onPopoverEvent()
.
<script setup>
import { usePopoverEvent } from 'v-popover';
usePopoverEvent('popoverupdate', (e) => {
console.log('Direction changed for popover', e.detail.name);
}, {
name: 'my_popover',
id: 'my_popover_trigger',
updates: ['direction'],
})
</script>
Styling
VPopover provides customized styling support via the theme
and contentClass
properties.
Use theme
Basic light/dark mode styling can be optionally applied by setting the theme
property on components or the directives.
For example, the Tooltip
component uses the dark theme under the hood, but can be reset to the light theme by manually.
<template>
<div class="flex gap-2">
<Tooltip content="I am dark">
<BaseButton>Dark Tooltip</BaseButton>
</Tooltip>
<Tooltip theme="light" content="I am light">
<BaseButton>Light Tooltip</BaseButton>
</Tooltip>
</div>
</template>
Clearing theme
A theme can be cleared by setting theme
to a falsey value. This will clear all of the default styling for the popover content.
<template>
<Tooltip content="I have no styling applied" theme="">
<BaseButton>Dark Tooltip</BaseButton>
</Tooltip>
</template>
Since the theme
is now cleared, custom styling can be easily applied using contentClass
.
Use contentClass
The contentClass
property can be used to apply a custom class to the popover content window.
<template>
<Tooltip
content="WARNING: This is a dangerous operation"
content-class="font-bold bg-red-100 text-red-600 px-2 py-1 border border-red-300 rounded-lg"
:arrow-size="14"
theme=""
>
<BaseButton class="bg-red-500 hover:bg-red-700">Dangerous Action</BaseButton>
</Tooltip>
</template>
Installation
Vue.js 3.2+ is required
Install Plugin
// npm
npm install v-popover
// yarn
yarn add v-popover
Use Plugin
As of v3.0.0-alpha.7
, all installation methods require manual import of component styles. This is due to Vite build restrictions in libary mode.
import 'v-popover/style.css';
Method 1: Use Globally
import VPopover from 'v-popover';
import 'v-popover/style.css';
app.use(VPopover, {})
<!--MyComponent.vue-->
<template>
<VPopover />
</template>
Method 2: Use Components Globally
import { PopoverTrigger, PopoverContent } from 'v-popover';
import 'v-popover/style.css';
app.use(setupCalendar, {})
app.component('PopoverTrigger', PopoverTrigger)
app.component('PopoverContent', PopoverContent)
<!--MyComponent.vue-->
<template>
<PopoverTrigger />
<PopoverContent />
</template>
Method 3: Use Components As Needed
<!--MyComponent.vue-->
<template>
<PopoverTrigger>
<button type="button">Show content</button>
<PopoverTrigger>
<PopoverContent>
Custom content here
</PopoverContent>
<DatePicker v-model="date">
</template>
<script setup>
import { PopoverTrigger, PopoverContent } from 'v-popover';
import 'v-popover/style.css';
</script>