Popover
This library is a work-in-progress. We are releasing it early to gather feedback, but it is not ready for production.
A popover is a floating element that displays rich content in a panel anchored to a trigger element. Unlike tooltips which show brief hints, popovers can contain interactive content like forms, buttons, and complex layouts.
Examples
Live Preview
Basic Popover
This is the popover content. You can put any content here including text, links, buttons, and more.
Popovers can contain rich content and interactive elements.
Popovers can be triggered from links too.
Trigger Types
This popover opens when you click the button.
This popover opens when you hover over the button.
This popover opens when the button receives focus (keyboard navigation).
Placement Options
Popover positioned above
Popover positioned to the right
Popover positioned below
Popover positioned to the left
Rich Content
Without Close Button
This popover has no close button. Click outside to close.
This popover doesn't have an arrow pointing to the trigger.
Event Handling
Listen to show and hide events to track when the popover opens and closes.
Show events: 0
Hide events: 0
Open and close this popover to see the event counts above.
CSS Shadow Parts Customization
Use CSS Shadow Parts to customize the popover's appearance without affecting the component's internal styling. One drawback is that the arrow part can be tricky to style due to its border-based implementation so we hide it in these examples.
This popover has been customized with CSS Shadow Parts for a gradient background!
A success-themed popover with custom styling.
View Vue Code
<template>
<section>
<div class="mbe4">
<h2>Basic Popover</h2>
</div>
<div class="stacked-mobile mbe4">
<VuePopover>
<button slot="trigger">Open Popover</button>
<span slot="title">Popover Title</span>
<div slot="content">
<p>This is the popover content. You can put any content here including text, links, buttons, and more.</p>
</div>
</VuePopover>
<VuePopover>
<VueButton
slot="trigger"
variant="primary"
>
Button Trigger
</VueButton>
<span slot="title">User Information</span>
<div slot="content">
<p>Popovers can contain rich content and interactive elements.</p>
</div>
</VuePopover>
<VuePopover>
<a
href="#"
slot="trigger"
style="text-decoration: underline; cursor: pointer;"
@click.prevent
>
Link Trigger
</a>
<span slot="title">Link Popover</span>
<div slot="content">
<p>Popovers can be triggered from links too.</p>
</div>
</VuePopover>
<VuePopover>
<button
slot="trigger"
style="background: none; border: none; cursor: pointer; padding: 8px; display: flex; align-items: center; gap: 4px;"
aria-label="More options"
>
<MoreVertical :size="20" />
</button>
<span slot="title">More Options</span>
<div slot="content">
<div style="display: flex; flex-direction: column; gap: 8px;">
<button style="text-align: left; padding: 8px; background: none; border: none; cursor: pointer; border-radius: var(--ag-radius-sm);">
Edit
</button>
<button style="text-align: left; padding: 8px; background: none; border: none; cursor: pointer; border-radius: var(--ag-radius-sm);">
Share
</button>
<button style="text-align: left; padding: 8px; background: none; border: none; cursor: pointer; color: var(--ag-error); border-radius: var(--ag-radius-sm);">
Delete
</button>
</div>
</div>
</VuePopover>
<VuePopover>
<button
slot="trigger"
style="background: none; border: none; cursor: pointer; padding: 8px;"
aria-label="Menu"
>
<Menu :size="24" />
</button>
<span slot="title">Navigation Menu</span>
<div slot="content">
<nav style="display: flex; flex-direction: column; gap: 4px;">
<a
href="#"
style="padding: 8px; text-decoration: none; color: var(--ag-text-primary); border-radius: var(--ag-radius-sm);"
>Home</a>
<a
href="#"
style="padding: 8px; text-decoration: none; color: var(--ag-text-primary); border-radius: var(--ag-radius-sm);"
>About</a>
<a
href="#"
style="padding: 8px; text-decoration: none; color: var(--ag-text-primary); border-radius: var(--ag-radius-sm);"
>Contact</a>
</nav>
</div>
</VuePopover>
</div>
<div class="mbe4">
<h2>Trigger Types</h2>
</div>
<div class="stacked-mobile mbe4">
<VuePopover trigger-type="click">
<VueButton slot="trigger">Click Trigger</VueButton>
<span slot="title">Click Popover</span>
<div slot="content">
<p>This popover opens when you click the button.</p>
</div>
</VuePopover>
<VuePopover trigger-type="hover">
<VueButton
slot="trigger"
variant="secondary"
>
Hover Trigger
</VueButton>
<span slot="title">Hover Popover</span>
<div slot="content">
<p>This popover opens when you hover over the button.</p>
</div>
</VuePopover>
<VuePopover trigger-type="focus">
<VueButton
slot="trigger"
variant="success"
>
Focus Trigger
</VueButton>
<span slot="title">Focus Popover</span>
<div slot="content">
<p>This popover opens when the button receives focus (keyboard navigation).</p>
</div>
</VuePopover>
</div>
<div class="mbe4">
<h2>Placement Options</h2>
</div>
<div
class="mbe4"
style="display: grid; grid-template-columns: 1fr 1fr 1fr; grid-template-rows: auto auto auto; gap: 1rem; place-items: center; max-width: 275px; margin: 0 auto;"
>
<div style="grid-column: 2; grid-row: 1;">
<VuePopover placement="top">
<VueButton slot="trigger">Top</VueButton>
<span slot="title">Top Placement</span>
<div slot="content">
<p>Popover positioned above</p>
</div>
</VuePopover>
</div>
<div style="grid-column: 3; grid-row: 2;">
<VuePopover placement="right">
<VueButton slot="trigger">Right</VueButton>
<span slot="title">Right Placement</span>
<div slot="content">
<p>Popover positioned to the right</p>
</div>
</VuePopover>
</div>
<div style="grid-column: 2; grid-row: 3;">
<VuePopover placement="bottom">
<VueButton slot="trigger">Bottom</VueButton>
<span slot="title">Bottom Placement</span>
<div slot="content">
<p>Popover positioned below</p>
</div>
</VuePopover>
</div>
<div style="grid-column: 1; grid-row: 2;">
<VuePopover placement="left">
<VueButton slot="trigger">Left</VueButton>
<span slot="title">Left Placement</span>
<div slot="content">
<p>Popover positioned to the left</p>
</div>
</VuePopover>
</div>
</div>
<div class="mbe4">
<h2>Rich Content</h2>
</div>
<div class="stacked-mobile mbe4">
<VuePopover>
<VueButton
slot="trigger"
variant="primary"
>
User Profile
</VueButton>
<span slot="title">John Doe</span>
<div slot="content">
<div style="display: flex; flex-direction: column; gap: 12px;">
<div style="display: flex; align-items: center; gap: 12px;">
<div style="width: 48px; height: 48px; border-radius: 50%; background: display: flex; align-items: center; justify-content: center; color: white; font-weight: bold;">
JD
</div>
<div>
<div style="font-weight: 600;">John Doe</div>
<div style="font-size: 14px; color: var(--ag-text-secondary);">john@example.com</div>
</div>
</div>
<div style="padding-top: 8px; border-top: 1px solid var(--ag-border);">
<VueButton
variant="primary"
style="width: 100%;"
>
View Profile
</VueButton>
</div>
</div>
</div>
</VuePopover>
<VuePopover>
<VueButton
slot="trigger"
variant="secondary"
>
Add Comment
</VueButton>
<span slot="title">New Comment</span>
<div slot="content">
<form
style="display: flex; flex-direction: column; gap: 12px; min-width: 250px;"
@submit.prevent
>
<div>
<label style="display: block; margin-bottom: 4px; font-size: 14px;">Name</label>
<input
type="text"
placeholder="Your name"
style="width: 100%; padding: 8px; border: 1px solid var(--ag-border); border-radius: var(--ag-radius-sm); box-sizing: border-box;"
>
</div>
<div>
<label style="display: block; margin-bottom: 4px; font-size: 14px;">Comment</label>
<textarea
placeholder="Your comment"
rows="3"
style="width: 100%; padding: 8px; border: 1px solid var(--ag-border); border-radius: var(--ag-radius-sm); resize: vertical; box-sizing: border-box;"
/>
</div>
<VueButton
variant="primary"
type="submit"
>
Submit
</VueButton>
</form>
</div>
</VuePopover>
</div>
<div class="mbe4">
<h2>Without Close Button</h2>
</div>
<div class="stacked-mobile mbe4">
<VuePopover :show-close-button="false">
<VueButton slot="trigger">No Close Button</VueButton>
<span slot="title">Title Only</span>
<div slot="content">
<p>This popover has no close button. Click outside to close.</p>
</div>
</VuePopover>
<VuePopover :arrow="false">
<VueButton
slot="trigger"
variant="secondary"
>
No Arrow
</VueButton>
<span slot="title">Popover Without Arrow</span>
<div slot="content">
<p>This popover doesn't have an arrow pointing to the trigger.</p>
</div>
</VuePopover>
</div>
<div class="mbe4">
<h2>Event Handling</h2>
<p class="mbs2 mbe3">
Listen to show and hide events to track when the popover opens and closes.
</p>
<div style="margin-bottom: 16px; padding: 12px; background: var(--ag-background-secondary); border-radius: var(--ag-radius-md); border: 1px solid var(--ag-border);">
<p style="margin: 4px 0; font-size: 14px;">
<strong>Show events:</strong> {{ showCount }}
</p>
<p style="margin: 4px 0; font-size: 14px;">
<strong>Hide events:</strong> {{ hideCount }}
</p>
</div>
</div>
<div class="stacked-mobile mbe4">
<VuePopover
@show="handleShow"
@hide="handleHide"
>
<VueButton
slot="trigger"
variant="primary"
>
Toggle Popover
</VueButton>
<span slot="title">Event Tracking</span>
<div slot="content">
<p>Open and close this popover to see the event counts above.</p>
</div>
</VuePopover>
</div>
<div class="mbe4">
<h2>CSS Shadow Parts Customization</h2>
<p class="mbs2 mbe3">
Use CSS Shadow Parts to customize the popover's appearance without affecting the component's internal styling. One drawback is that the arrow part can be tricky to style due to its border-based implementation so we hide it in these examples.
</p>
</div>
<div class="stacked-mobile mbe4">
<VuePopover class="custom-popover-gradient">
<VueButton
slot="trigger"
variant="primary"
>
Gradient Popover
</VueButton>
<span
slot="title"
style="color: white;"
>Customized Style</span>
<div
slot="content"
style="color: white;"
>
<p style="color: white;">This popover has been customized with CSS Shadow Parts for a gradient background!</p>
</div>
</VuePopover>
<VuePopover class="custom-popover-success">
<VueButton
slot="trigger"
variant="success"
>
Success Popover
</VueButton>
<span
slot="title"
style="color: white;"
>Success Theme</span>
<div
slot="content"
style="color: white;"
>
<p style="color: white;">A success-themed popover with custom styling.</p>
</div>
</VuePopover>
</div>
</section>
</template>
<script>
import { VuePopover } from "agnosticui-core/popover/vue";
import VueButton from "agnosticui-core/button/vue";
import { Menu, MoreVertical } from "lucide-vue-next";
import { ref } from "vue";
export default {
name: "PopoverExamples",
components: {
VuePopover,
VueButton,
Menu,
MoreVertical,
},
setup() {
const showCount = ref(0);
const hideCount = ref(0);
const handleShow = () => {
showCount.value++;
};
const handleHide = () => {
hideCount.value++;
};
return {
showCount,
hideCount,
handleShow,
handleHide,
};
},
};
</script>
<style>
/* CSS Shadow Parts customization examples */
.custom-popover-gradient::part(ag-popover) {
background: linear-gradient(135deg, #4338ca 0%, #6b21a8 100%);
color: white;
border: none;
box-shadow: 0 20px 40px rgba(67, 56, 202, 0.5);
}
/* How to match a custom gradient? Just hide ¯\_(ツ)_/¯ */
.custom-popover-gradient::part(ag-popover-arrow) {
display: none;
}
.custom-popover-success::part(ag-popover) {
background: #059669;
color: white;
border: 2px solid #047857;
box-shadow: 0 10px 25px rgba(5, 150, 105, 0.3);
}
/* The border-based arrow, floating-ui's flip, and other complexities makes
the ROI on having an arrow questionable. So, we just hide ¯\_(ツ)_/¯ */
.custom-popover-success::part(ag-popover-arrow) {
display: none;
}
</style>
Live Preview
View Lit / Web Component Code
import { LitElement, html } from 'lit';
import 'agnosticui-core/popover';
import 'agnosticui-core/button';
export class PopoverLitExamples extends LitElement {
static properties = {
showCount: { type: Number },
hideCount: { type: Number }
};
constructor() {
super();
this.showCount = 0;
this.hideCount = 0;
}
// Render in light DOM to access global utility classes
createRenderRoot() {
return this;
}
handleShow() {
this.showCount++;
}
handleHide() {
this.hideCount++;
}
render() {
return html`
<section>
<div class="mbe4">
<h2>Basic Popover</h2>
</div>
<div class="stacked-mobile mbe4">
<ag-popover>
<button slot="trigger">Open Popover</button>
<span slot="title">Popover Title</span>
<div slot="content">
<p>This is the popover content. You can put any content here including text, links, buttons, and more.</p>
</div>
</ag-popover>
<ag-popover>
<ag-button slot="trigger" variant="primary">Button Trigger</ag-button>
<span slot="title">User Information</span>
<div slot="content">
<p>Popovers can contain rich content and interactive elements.</p>
</div>
</ag-popover>
<ag-popover>
<a
href="#"
slot="trigger"
style="text-decoration: underline; cursor: pointer;"
@click=${(e) => e.preventDefault()}
>
Link Trigger
</a>
<span slot="title">Link Popover</span>
<div slot="content">
<p>Popovers can be triggered from links too.</p>
</div>
</ag-popover>
<ag-popover>
<button
slot="trigger"
style="background: none; border: none; cursor: pointer; padding: 8px; display: flex; align-items: center; gap: 4px;"
aria-label="More options"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="1"></circle>
<circle cx="12" cy="5" r="1"></circle>
<circle cx="12" cy="19" r="1"></circle>
</svg>
</button>
<span slot="title">More Options</span>
<div slot="content">
<div style="display: flex; flex-direction: column; gap: 8px;">
<button style="text-align: left; padding: 8px; background: none; border: none; cursor: pointer; border-radius: var(--ag-radius-sm);">
Edit
</button>
<button style="text-align: left; padding: 8px; background: none; border: none; cursor: pointer; border-radius: var(--ag-radius-sm);">
Share
</button>
<button style="text-align: left; padding: 8px; background: none; border: none; cursor: pointer; color: var(--ag-error); border-radius: var(--ag-radius-sm);">
Delete
</button>
</div>
</div>
</ag-popover>
<ag-popover>
<button
slot="trigger"
style="background: none; border: none; cursor: pointer; padding: 8px;"
aria-label="Menu"
>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="3" y1="12" x2="21" y2="12"></line>
<line x1="3" y1="6" x2="21" y2="6"></line>
<line x1="3" y1="18" x2="21" y2="18"></line>
</svg>
</button>
<span slot="title">Navigation Menu</span>
<div slot="content">
<nav style="display: flex; flex-direction: column; gap: 4px;">
<a
href="#"
style="padding: 8px; text-decoration: none; color: var(--ag-text-primary); border-radius: var(--ag-radius-sm);"
>Home</a>
<a
href="#"
style="padding: 8px; text-decoration: none; color: var(--ag-text-primary); border-radius: var(--ag-radius-sm);"
>About</a>
<a
href="#"
style="padding: 8px; text-decoration: none; color: var(--ag-text-primary); border-radius: var(--ag-radius-sm);"
>Contact</a>
</nav>
</div>
</ag-popover>
</div>
<div class="mbe4">
<h2>Trigger Types</h2>
</div>
<div class="stacked-mobile mbe4">
<ag-popover trigger-type="click">
<ag-button slot="trigger">Click Trigger</ag-button>
<span slot="title">Click Popover</span>
<div slot="content">
<p>This popover opens when you click the button.</p>
</div>
</ag-popover>
<ag-popover trigger-type="hover">
<ag-button slot="trigger" variant="secondary">Hover Trigger</ag-button>
<span slot="title">Hover Popover</span>
<div slot="content">
<p>This popover opens when you hover over the button.</p>
</div>
</ag-popover>
<ag-popover trigger-type="focus">
<ag-button slot="trigger" variant="success">Focus Trigger</ag-button>
<span slot="title">Focus Popover</span>
<div slot="content">
<p>This popover opens when the button receives focus (keyboard navigation).</p>
</div>
</ag-popover>
</div>
<div class="mbe4">
<h2>Placement Options</h2>
</div>
<div
class="mbe4"
style="display: grid; grid-template-columns: 1fr 1fr 1fr; grid-template-rows: auto auto auto; gap: 1rem; place-items: center; max-width: 275px; margin: 0 auto;"
>
<div style="grid-column: 2; grid-row: 1;">
<ag-popover placement="top">
<ag-button slot="trigger">Top</ag-button>
<span slot="title">Top Placement</span>
<div slot="content">
<p>Popover positioned above</p>
</div>
</ag-popover>
</div>
<div style="grid-column: 3; grid-row: 2;">
<ag-popover placement="right">
<ag-button slot="trigger">Right</ag-button>
<span slot="title">Right Placement</span>
<div slot="content">
<p>Popover positioned to the right</p>
</div>
</ag-popover>
</div>
<div style="grid-column: 2; grid-row: 3;">
<ag-popover placement="bottom">
<ag-button slot="trigger">Bottom</ag-button>
<span slot="title">Bottom Placement</span>
<div slot="content">
<p>Popover positioned below</p>
</div>
</ag-popover>
</div>
<div style="grid-column: 1; grid-row: 2;">
<ag-popover placement="left">
<ag-button slot="trigger">Left</ag-button>
<span slot="title">Left Placement</span>
<div slot="content">
<p>Popover positioned to the left</p>
</div>
</ag-popover>
</div>
</div>
<div class="mbe4">
<h2>Rich Content</h2>
</div>
<div class="stacked-mobile mbe4">
<ag-popover>
<ag-button slot="trigger" variant="primary">User Profile</ag-button>
<span slot="title">John Doe</span>
<div slot="content">
<div style="display: flex; flex-direction: column; gap: 12px;">
<div style="display: flex; align-items: center; gap: 12px;">
<div style="width: 48px; height: 48px; border-radius: 50%; background: var(--ag-primary); display: flex; align-items: center; justify-content: center; color: white; font-weight: bold;">
JD
</div>
<div>
<div style="font-weight: 600;">John Doe</div>
<div style="font-size: 14px; color: var(--ag-text-secondary);">john@example.com</div>
</div>
</div>
<div style="padding-top: 8px; border-top: 1px solid var(--ag-border);">
<ag-button variant="primary" style="width: 100%;">View Profile</ag-button>
</div>
</div>
</div>
</ag-popover>
<ag-popover>
<ag-button slot="trigger" variant="secondary">Add Comment</ag-button>
<span slot="title">New Comment</span>
<div slot="content">
<form
style="display: flex; flex-direction: column; gap: 12px; min-width: 250px;"
@submit=${(e) => e.preventDefault()}
>
<div>
<label style="display: block; margin-bottom: 4px; font-size: 14px;">Name</label>
<input
type="text"
placeholder="Your name"
style="width: 100%; padding: 8px; border: 1px solid var(--ag-border); border-radius: var(--ag-radius-sm); box-sizing: border-box;"
>
</div>
<div>
<label style="display: block; margin-bottom: 4px; font-size: 14px;">Comment</label>
<textarea
placeholder="Your comment"
rows="3"
style="width: 100%; padding: 8px; border: 1px solid var(--ag-border); border-radius: var(--ag-radius-sm); resize: vertical; box-sizing: border-box;"
></textarea>
</div>
<ag-button variant="primary" type="submit">Submit</ag-button>
</form>
</div>
</ag-popover>
</div>
<div class="mbe4">
<h2>Without Close Button</h2>
</div>
<div class="stacked-mobile mbe4">
<ag-popover show-close-button="false">
<ag-button slot="trigger">No Close Button</ag-button>
<span slot="title">Title Only</span>
<div slot="content">
<p>This popover has no close button. Click outside to close.</p>
</div>
</ag-popover>
<ag-popover arrow="false">
<ag-button slot="trigger" variant="secondary">No Arrow</ag-button>
<span slot="title">Popover Without Arrow</span>
<div slot="content">
<p>This popover doesn't have an arrow pointing to the trigger.</p>
</div>
</ag-popover>
</div>
<div class="mbe4">
<h2>Event Handling</h2>
<p class="mbs2 mbe3">
Listen to show and hide events to track when the popover opens and closes.
</p>
<div style="margin-bottom: 16px; padding: 12px; background: var(--ag-background-secondary); border-radius: var(--ag-radius-md); border: 1px solid var(--ag-border);">
<p style="margin: 4px 0; font-size: 14px;">
<strong>Show events:</strong> ${this.showCount}
</p>
<p style="margin: 4px 0; font-size: 14px;">
<strong>Hide events:</strong> ${this.hideCount}
</p>
</div>
</div>
<div class="stacked-mobile mbe4">
<ag-popover
@show=${this.handleShow}
@hide=${this.handleHide}
>
<ag-button slot="trigger" variant="primary">Toggle Popover</ag-button>
<span slot="title">Event Tracking</span>
<div slot="content">
<p>Open and close this popover to see the event counts above.</p>
</div>
</ag-popover>
</div>
<div class="mbe4">
<h2>CSS Shadow Parts Customization</h2>
<p class="mbs2 mbe3">
Use CSS Shadow Parts to customize the popover's appearance without affecting the component's internal styling. One drawback is that the arrow part can be tricky to style due to its border-based implementation so we hide it in these examples.
</p>
</div>
<div class="stacked-mobile mbe4">
<ag-popover class="custom-popover-gradient">
<ag-button slot="trigger" variant="primary">Gradient Popover</ag-button>
<span slot="title" style="color: white;">Customized Style</span>
<div slot="content" style="color: white;">
<p style="color: white;">This popover has been customized with CSS Shadow Parts for a gradient background!</p>
</div>
</ag-popover>
<ag-popover class="custom-popover-success">
<ag-button slot="trigger" variant="success">Success Popover</ag-button>
<span slot="title" style="color: white;">Success Theme</span>
<div slot="content" style="color: white;">
<p style="color: white;">A success-themed popover with custom styling.</p>
</div>
</ag-popover>
</div>
</section>
<style>
/* CSS Shadow Parts customization examples */
.custom-popover-gradient::part(ag-popover) {
background: linear-gradient(135deg, #4338ca 0%, #6b21a8 100%);
color: white;
border: none;
box-shadow: 0 20px 40px rgba(67, 56, 202, 0.5);
}
/* How to match a custom gradient? Just hide ¯\\_(ツ)_/¯ */
.custom-popover-gradient::part(ag-popover-arrow) {
display: none;
}
.custom-popover-success::part(ag-popover) {
background: #059669;
color: white;
border: 2px solid #047857;
box-shadow: 0 10px 25px rgba(5, 150, 105, 0.3);
}
/* The border-based arrow, floating-ui's flip, and other complexities makes
the ROI on having an arrow questionable. So, we just hide ¯\\_(ツ)_/¯ */
.custom-popover-success::part(ag-popover-arrow) {
display: none;
}
</style>
`;
}
}
// Register the custom element
customElements.define('popover-lit-examples', PopoverLitExamples);
Interactive Preview: Click the "Open in StackBlitz" button below to see this example running live in an interactive playground.
View React Code
import { useState } from "react";
import { ReactPopover } from "agnosticui-core/popover/react";
import { ReactButton } from "agnosticui-core/button/react";
// Simple SVG icon components
const MoreVertical = () => (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<circle cx="12" cy="12" r="1"></circle>
<circle cx="12" cy="5" r="1"></circle>
<circle cx="12" cy="19" r="1"></circle>
</svg>
);
const Menu = () => (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<line x1="3" y1="12" x2="21" y2="12"></line>
<line x1="3" y1="6" x2="21" y2="6"></line>
<line x1="3" y1="18" x2="21" y2="18"></line>
</svg>
);
export default function PopoverReactExamples() {
const [showCount, setShowCount] = useState(0);
const [hideCount, setHideCount] = useState(0);
const handleShow = () => {
setShowCount(showCount + 1);
};
const handleHide = () => {
setHideCount(hideCount + 1);
};
return (
<section>
<div className="mbe4">
<h2>Basic Popover</h2>
</div>
<div className="stacked-mobile mbe4">
<ReactPopover>
<button slot="trigger">Open Popover</button>
<span slot="title">Popover Title</span>
<div slot="content">
<p>This is the popover content. You can put any content here including text, links, buttons, and more.</p>
</div>
</ReactPopover>
<ReactPopover>
<ReactButton slot="trigger" variant="primary">Button Trigger</ReactButton>
<span slot="title">User Information</span>
<div slot="content">
<p>Popovers can contain rich content and interactive elements.</p>
</div>
</ReactPopover>
<ReactPopover>
<a
href="#"
slot="trigger"
style={{ textDecoration: "underline", cursor: "pointer" }}
onClick={(e) => e.preventDefault()}
>
Link Trigger
</a>
<span slot="title">Link Popover</span>
<div slot="content">
<p>Popovers can be triggered from links too.</p>
</div>
</ReactPopover>
<ReactPopover>
<button
slot="trigger"
style={{ background: "none", border: "none", cursor: "pointer", padding: "8px", display: "flex", alignItems: "center", gap: "4px" }}
aria-label="More options"
>
<MoreVertical />
</button>
<span slot="title">More Options</span>
<div slot="content">
<div style={{ display: "flex", flexDirection: "column", gap: "8px" }}>
<button style={{ textAlign: "left", padding: "8px", background: "none", border: "none", cursor: "pointer", borderRadius: "var(--ag-radius-sm)" }}>
Edit
</button>
<button style={{ textAlign: "left", padding: "8px", background: "none", border: "none", cursor: "pointer", borderRadius: "var(--ag-radius-sm)" }}>
Share
</button>
<button style={{ textAlign: "left", padding: "8px", background: "none", border: "none", cursor: "pointer", color: "var(--ag-error)", borderRadius: "var(--ag-radius-sm)" }}>
Delete
</button>
</div>
</div>
</ReactPopover>
<ReactPopover>
<button
slot="trigger"
style={{ background: "none", border: "none", cursor: "pointer", padding: "8px" }}
aria-label="Menu"
>
<Menu />
</button>
<span slot="title">Navigation Menu</span>
<div slot="content">
<nav style={{ display: "flex", flexDirection: "column", gap: "4px" }}>
<a
href="#"
style={{ padding: "8px", textDecoration: "none", color: "var(--ag-text-primary)", borderRadius: "var(--ag-radius-sm)" }}
>Home</a>
<a
href="#"
style={{ padding: "8px", textDecoration: "none", color: "var(--ag-text-primary)", borderRadius: "var(--ag-radius-sm)" }}
>About</a>
<a
href="#"
style={{ padding: "8px", textDecoration: "none", color: "var(--ag-text-primary)", borderRadius: "var(--ag-radius-sm)" }}
>Contact</a>
</nav>
</div>
</ReactPopover>
</div>
<div className="mbe4">
<h2>Trigger Types</h2>
</div>
<div className="stacked-mobile mbe4">
<ReactPopover triggerType="click">
<ReactButton slot="trigger">Click Trigger</ReactButton>
<span slot="title">Click Popover</span>
<div slot="content">
<p>This popover opens when you click the button.</p>
</div>
</ReactPopover>
<ReactPopover triggerType="hover">
<ReactButton slot="trigger" variant="secondary">Hover Trigger</ReactButton>
<span slot="title">Hover Popover</span>
<div slot="content">
<p>This popover opens when you hover over the button.</p>
</div>
</ReactPopover>
<ReactPopover triggerType="focus">
<ReactButton slot="trigger" variant="success">Focus Trigger</ReactButton>
<span slot="title">Focus Popover</span>
<div slot="content">
<p>This popover opens when the button receives focus (keyboard navigation).</p>
</div>
</ReactPopover>
</div>
<div className="mbe4">
<h2>Placement Options</h2>
</div>
<div
className="mbe4"
style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gridTemplateRows: "auto auto auto", gap: "1rem", placeItems: "center", maxWidth: "275px", margin: "0 auto" }}
>
<div style={{ gridColumn: "2", gridRow: "1" }}>
<ReactPopover placement="top">
<ReactButton slot="trigger">Top</ReactButton>
<span slot="title">Top Placement</span>
<div slot="content">
<p>Popover positioned above</p>
</div>
</ReactPopover>
</div>
<div style={{ gridColumn: "3", gridRow: "2" }}>
<ReactPopover placement="right">
<ReactButton slot="trigger">Right</ReactButton>
<span slot="title">Right Placement</span>
<div slot="content">
<p>Popover positioned to the right</p>
</div>
</ReactPopover>
</div>
<div style={{ gridColumn: "2", gridRow: "3" }}>
<ReactPopover placement="bottom">
<ReactButton slot="trigger">Bottom</ReactButton>
<span slot="title">Bottom Placement</span>
<div slot="content">
<p>Popover positioned below</p>
</div>
</ReactPopover>
</div>
<div style={{ gridColumn: "1", gridRow: "2" }}>
<ReactPopover placement="left">
<ReactButton slot="trigger">Left</ReactButton>
<span slot="title">Left Placement</span>
<div slot="content">
<p>Popover positioned to the left</p>
</div>
</ReactPopover>
</div>
</div>
<div className="mbe4">
<h2>Rich Content</h2>
</div>
<div className="stacked-mobile mbe4">
<ReactPopover>
<ReactButton slot="trigger" variant="primary">User Profile</ReactButton>
<span slot="title">John Doe</span>
<div slot="content">
<div style={{ display: "flex", flexDirection: "column", gap: "12px" }}>
<div style={{ display: "flex", alignItems: "center", gap: "12px" }}>
<div style={{ width: "48px", height: "48px", borderRadius: "50%", background: "var(--ag-primary)", display: "flex", alignItems: "center", justifyContent: "center", color: "white", fontWeight: "bold" }}>
JD
</div>
<div>
<div style={{ fontWeight: "600" }}>John Doe</div>
<div style={{ fontSize: "14px", color: "var(--ag-text-secondary)" }}>john@example.com</div>
</div>
</div>
<div style={{ paddingTop: "8px", borderTop: "1px solid var(--ag-border)" }}>
<ReactButton variant="primary" style={{ width: "100%" }}>View Profile</ReactButton>
</div>
</div>
</div>
</ReactPopover>
<ReactPopover>
<ReactButton slot="trigger" variant="secondary">Add Comment</ReactButton>
<span slot="title">New Comment</span>
<div slot="content">
<form
style={{ display: "flex", flexDirection: "column", gap: "12px", minWidth: "250px" }}
onSubmit={(e) => e.preventDefault()}
>
<div>
<label style={{ display: "block", marginBottom: "4px", fontSize: "14px" }}>Name</label>
<input
type="text"
placeholder="Your name"
style={{ width: "100%", padding: "8px", border: "1px solid var(--ag-border)", borderRadius: "var(--ag-radius-sm)", boxSizing: "border-box" }}
/>
</div>
<div>
<label style={{ display: "block", marginBottom: "4px", fontSize: "14px" }}>Comment</label>
<textarea
placeholder="Your comment"
rows="3"
style={{ width: "100%", padding: "8px", border: "1px solid var(--ag-border)", borderRadius: "var(--ag-radius-sm)", resize: "vertical", boxSizing: "border-box" }}
/>
</div>
<ReactButton variant="primary" type="submit">Submit</ReactButton>
</form>
</div>
</ReactPopover>
</div>
<div className="mbe4">
<h2>Without Close Button</h2>
</div>
<div className="stacked-mobile mbe4">
<ReactPopover showCloseButton={false}>
<ReactButton slot="trigger">No Close Button</ReactButton>
<span slot="title">Title Only</span>
<div slot="content">
<p>This popover has no close button. Click outside to close.</p>
</div>
</ReactPopover>
<ReactPopover arrow={false}>
<ReactButton slot="trigger" variant="secondary">No Arrow</ReactButton>
<span slot="title">Popover Without Arrow</span>
<div slot="content">
<p>This popover doesn't have an arrow pointing to the trigger.</p>
</div>
</ReactPopover>
</div>
<div className="mbe4">
<h2>Event Handling</h2>
<p className="mbs2 mbe3">
Listen to show and hide events to track when the popover opens and closes.
</p>
<div style={{ marginBottom: "16px", padding: "12px", background: "var(--ag-background-secondary)", borderRadius: "var(--ag-radius-md)", border: "1px solid var(--ag-border)" }}>
<p style={{ margin: "4px 0", fontSize: "14px" }}>
<strong>Show events:</strong> {showCount}
</p>
<p style={{ margin: "4px 0", fontSize: "14px" }}>
<strong>Hide events:</strong> {hideCount}
</p>
</div>
</div>
<div className="stacked-mobile mbe4">
<ReactPopover
onShow={handleShow}
onHide={handleHide}
>
<ReactButton slot="trigger" variant="primary">Toggle Popover</ReactButton>
<span slot="title">Event Tracking</span>
<div slot="content">
<p>Open and close this popover to see the event counts above.</p>
</div>
</ReactPopover>
</div>
<div className="mbe4">
<h2>CSS Shadow Parts Customization</h2>
<p className="mbs2 mbe3">
Use CSS Shadow Parts to customize the popover's appearance without affecting the component's internal styling. One drawback is that the arrow part can be tricky to style due to its border-based implementation so we hide it in these examples.
</p>
</div>
<div className="stacked-mobile mbe4">
<ReactPopover className="custom-popover-gradient">
<ReactButton slot="trigger" variant="primary">Gradient Popover</ReactButton>
<span slot="title" style={{ color: "white" }}>Customized Style</span>
<div slot="content" style={{ color: "white" }}>
<p style={{ color: "white" }}>This popover has been customized with CSS Shadow Parts for a gradient background!</p>
</div>
</ReactPopover>
<ReactPopover className="custom-popover-success">
<ReactButton slot="trigger" variant="success">Success Popover</ReactButton>
<span slot="title" style={{ color: "white" }}>Success Theme</span>
<div slot="content" style={{ color: "white" }}>
<p style={{ color: "white" }}>A success-themed popover with custom styling.</p>
</div>
</ReactPopover>
</div>
</section>
);
}
Usage
TIP
The framework examples below import AgnosticUI as an npm package. Alternatively, you can use the CLI for complete control, AI/LLM visibility, and full code ownership:
npx ag init --framework FRAMEWORK # react, vue, lit, svelte, etc.
npx ag add PopoverThe CLI copies source code directly into your project, giving you full visibility and control. After running npx ag add, you'll receive exact import instructions.
Vue
<template>
<section>
<VuePopover>
<button slot="trigger">Open Popover</button>
<span slot="title">Popover Title</span>
<div slot="content">
<p>This is the popover content.</p>
</div>
</VuePopover>
<VuePopover trigger-type="hover">
<VueButton slot="trigger">Hover Me</VueButton>
<span slot="title">Hover Popover</span>
<div slot="content">
<p>This popover opens on hover.</p>
</div>
</VuePopover>
<VuePopover placement="right">
<VueButton slot="trigger">Right Placement</VueButton>
<span slot="title">Positioned Right</span>
<div slot="content">
<p>This popover appears to the right of the trigger.</p>
</div>
</VuePopover>
<VuePopover>
<VueButton slot="trigger">User Profile</VueButton>
<span slot="title">John Doe</span>
<div slot="content">
<div style="display: flex; flex-direction: column; gap: 12px;">
<div>User profile information...</div>
<VueButton variant="primary">View Profile</VueButton>
</div>
</div>
</VuePopover>
<VuePopover
@show="handleShow"
@hide="handleHide"
>
<VueButton slot="trigger">With Events</VueButton>
<span slot="title">Event Tracking</span>
<div slot="content">
<p>Events fire when popover shows and hides.</p>
</div>
</VuePopover>
<VuePopover :show-close-button="false">
<VueButton slot="trigger">No Close Button</VueButton>
<span slot="title">Title Only</span>
<div slot="content">
<p>Click outside to close this popover.</p>
</div>
</VuePopover>
</section>
</template>
<script>
import { VuePopover } from "agnosticui-core/popover/vue";
import VueButton from "agnosticui-core/button/vue";
export default {
components: {
VuePopover,
VueButton,
},
methods: {
handleShow() {
console.log("Popover shown");
},
handleHide() {
console.log("Popover hidden");
},
},
};
</script>React
import { ReactPopover, PopoverTrigger, PopoverTitle, PopoverContent } from 'agnosticui-core/popover/react';
import { ReactButton } from 'agnosticui-core/button/react';
export default function PopoverExample() {
const handleShow = () => {
console.log("Popover shown");
};
const handleHide = () => {
console.log("Popover hidden");
};
return (
<section>
<ReactPopover>
<PopoverTrigger>
<button>Open Popover</button>
</PopoverTrigger>
<PopoverTitle>
<span>Popover Title</span>
</PopoverTitle>
<PopoverContent>
<p>This is the popover content.</p>
</PopoverContent>
</ReactPopover>
<ReactPopover triggerType="hover">
<PopoverTrigger>
<ReactButton>Hover Me</ReactButton>
</PopoverTrigger>
<PopoverTitle>
<span>Hover Popover</span>
</PopoverTitle>
<PopoverContent>
<p>This popover opens on hover.</p>
</PopoverContent>
</ReactPopover>
<ReactPopover placement="right">
<PopoverTrigger>
<ReactButton>Right Placement</ReactButton>
</PopoverTrigger>
<PopoverTitle>
<span>Positioned Right</span>
</PopoverTitle>
<PopoverContent>
<p>This popover appears to the right of the trigger.</p>
</PopoverContent>
</ReactPopover>
<ReactPopover>
<PopoverTrigger>
<ReactButton>User Profile</ReactButton>
</PopoverTrigger>
<PopoverTitle>
<span>John Doe</span>
</PopoverTitle>
<PopoverContent>
<div style={{ display: "flex", flexDirection: "column", gap: "12px" }}>
<div>User profile information...</div>
<ReactButton variant="primary">View Profile</ReactButton>
</div>
</PopoverContent>
</ReactPopover>
<ReactPopover
onShow={handleShow}
onHide={handleHide}
>
<PopoverTrigger>
<ReactButton>With Events</ReactButton>
</PopoverTrigger>
<PopoverTitle>
<span>Event Tracking</span>
</PopoverTitle>
<PopoverContent>
<p>Events fire when popover shows and hides.</p>
</PopoverContent>
</ReactPopover>
<ReactPopover showCloseButton={false}>
<PopoverTrigger>
<ReactButton>No Close Button</ReactButton>
</PopoverTrigger>
<PopoverTitle>
<span>Title Only</span>
</PopoverTitle>
<PopoverContent>
<p>Click outside to close this popover.</p>
</PopoverContent>
</ReactPopover>
</section>
);
}Lit (Web Components)
import { LitElement, html, css } from 'lit';
import { customElement } from 'lit/decorators.js';
import 'agnosticui-core/popover';
@customElement('popover-example')
export class PopoverExample extends LitElement {
static styles = css`
:host {
display: block;
}
section {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
`;
firstUpdated() {
// Set up event listeners for popover in the shadow DOM
const popover = this.shadowRoot?.querySelector('#event-popover');
popover?.addEventListener('show', (e: Event) => {
const customEvent = e as CustomEvent;
console.log('Popover shown', customEvent.detail);
});
popover?.addEventListener('hide', (e: Event) => {
const customEvent = e as CustomEvent;
console.log('Popover hidden', customEvent.detail);
});
}
render() {
return html`
<section>
<ag-popover>
<button slot="trigger">Open Popover</button>
<span slot="title">Popover Title</span>
<div slot="content">
<p>This is the popover content.</p>
</div>
</ag-popover>
<ag-popover trigger-type="hover">
<button slot="trigger">Hover Me</button>
<span slot="title">Hover Popover</span>
<div slot="content">
<p>This popover opens on hover.</p>
</div>
</ag-popover>
<ag-popover placement="right">
<button slot="trigger">Right Placement</button>
<span slot="title">Positioned Right</span>
<div slot="content">
<p>This popover appears to the right of the trigger.</p>
</div>
</ag-popover>
<ag-popover>
<button slot="trigger">User Profile</button>
<span slot="title">John Doe</span>
<div slot="content">
<div style="display: flex; flex-direction: column; gap: 12px;">
<div>User profile information...</div>
<button>View Profile</button>
</div>
</div>
</ag-popover>
<ag-popover id="event-popover">
<button slot="trigger">With Events</button>
<span slot="title">Event Tracking</span>
<div slot="content">
<p>Events fire when popover shows and hides.</p>
</div>
</ag-popover>
<ag-popover show-close-button="false">
<button slot="trigger">No Close Button</button>
<span slot="title">Title Only</span>
<div slot="content">
<p>Click outside to close this popover.</p>
</div>
</ag-popover>
</section>
`;
}
}Note: When using popover components within a custom element's shadow DOM, set up event listeners in the component's lifecycle (e.g., firstUpdated()) rather than using DOMContentLoaded, as document.querySelector() cannot access elements inside shadow DOM. Use this.shadowRoot.querySelector() instead.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
placement | 'top' | 'top-start' | 'top-end' | 'right' | 'right-start' | 'right-end' | 'bottom' | 'bottom-start' | 'bottom-end' | 'left' | 'left-start' | 'left-end' | 'bottom' | Placement of the popover relative to the trigger element |
distance | number | 8 | Distance in pixels between the popover and trigger element |
skidding | number | 0 | Offset in pixels along the alignment axis |
arrow | boolean | true | Whether to show an arrow pointing to the trigger element |
disabled | boolean | false | Prevents the popover from opening |
triggerType | 'click' | 'hover' | 'focus' | 'click' | How to trigger the popover (click, hover, or focus) |
matchTriggerWidth | boolean | false | Makes the popover's width match the trigger element's width |
showCloseButton | boolean | true | Whether to show a close button (×) in the popover header |
closeLabel | string | 'Close popover' | Accessible label for the close button (for screen readers) |
trapFocus | boolean | false | Whether to trap keyboard focus within the popover when open |
Events
| Event | Framework | Detail | Description |
|---|---|---|---|
show | Vue: @showReact: onShowLit: @show or .onShow | { visible: boolean } | Fired when the popover becomes visible. The visible property will be true. |
hide | Vue: @hideReact: onHideLit: @hide or .onHide | { visible: boolean } | Fired when the popover becomes hidden. The visible property will be false. |
Event Patterns
AgnosticUI Popover supports three event handling patterns:
- addEventListener (Lit/Vanilla JS):
const popover = document.querySelector("ag-popover");
popover.addEventListener("show", (e) => {
console.log("Popover shown:", e.detail.visible);
});
popover.addEventListener("hide", (e) => {
console.log("Popover hidden:", e.detail.visible);
});- Callback properties (Lit/Vanilla JS):
const popover = document.querySelector("ag-popover");
popover.onShow = (e) => {
console.log("Popover shown:", e.detail.visible);
};
popover.onHide = (e) => {
console.log("Popover hidden:", e.detail.visible);
};- Framework bindings (Vue/React):
<VuePopover @show="handleShow" @hide="handleHide">
</VuePopover><ReactPopover onShow={handleShow} onHide={handleHide}>
</ReactPopover>Event Handling Examples
Vue:
<VuePopover
@show="handleShow"
@hide="handleHide"
>
<button slot="trigger">Toggle Popover</button>
<span slot="title">Event Example</span>
<div slot="content">
<p>Popover content</p>
</div>
</VuePopover>
<script>
export default {
methods: {
handleShow(event) {
console.log("Popover opened", event.detail.visible);
},
handleHide(event) {
console.log("Popover closed", event.detail.visible);
},
},
};
</script>React:
<ReactPopover
onShow={(e) => console.log("Popover opened", e.detail.visible)}
onHide={(e) => console.log("Popover closed", e.detail.visible)}
>
<PopoverTrigger>
<button>Toggle Popover</button>
</PopoverTrigger>
<PopoverTitle>
<span>Event Example</span>
</PopoverTitle>
<PopoverContent>
<p>Popover content</p>
</PopoverContent>
</ReactPopover>Lit:
<script>
const popover = document.querySelector("ag-popover");
popover.addEventListener("show", (e) => {
console.log("Popover opened", e.detail.visible);
});
popover.addEventListener("hide", (e) => {
console.log("Popover closed", e.detail.visible);
});
</script>
<ag-popover id="my-popover"></ag-popover>
<script>
const popover = document.querySelector("#my-popover");
popover.onShow = (e) => console.log("Popover opened", e.detail.visible);
popover.onHide = (e) => console.log("Popover closed", e.detail.visible);
</script>Slots
Vue
- slot="trigger": The trigger element that opens the popover when interacted with
- slot="title": Optional title text displayed in the popover header
- slot="content": The main content of the popover
React
- PopoverTrigger: Component wrapper for the trigger element
- PopoverTitle: Component wrapper for the title content
- PopoverContent: Component wrapper for the main content
Lit
- slot="trigger": The trigger element that opens the popover
- slot="title": Optional title displayed in the popover header
- slot="content": The main content of the popover
CSS Shadow Parts
Shadow Parts allow you to style internal elements of the popover from outside the shadow DOM using the ::part() CSS selector.
| Part | Description |
|---|---|
ag-popover | The main popover container element that displays the content |
ag-popover-arrow | The arrow element that points to the trigger element |
ag-popover-close | The close button element inside the popover header |
Customization Example
ag-popover::part(ag-popover) {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 12px;
box-shadow: 0 20px 40px rgba(102, 126, 234, 0.4);
padding: 1.5rem;
}
ag-popover::part(ag-popover-arrow) {
background: #667eea;
border-color: #667eea;
}
ag-popover::part(ag-popover-close) {
background: rgba(255, 255, 255, 0.2);
color: white;
border-radius: 50%;
padding: 4px;
}
.success-popover::part(ag-popover) {
background: #14854f;
color: white;
border: 2px solid #059669;
box-shadow: 0 10px 25px rgba(16, 185, 129, 0.3);
}
.success-popover::part(ag-popover-arrow) {
background: #14854f;
border-color: #059669;
}Accessibility
The Popover implements the WAI-ARIA Dialog Pattern for non-modal overlays:
- Uses
role="dialog"andaria-modal="false"for proper screen reader announcement - Trigger element has
aria-expandedandaria-haspopupattributes - Pressing Escape closes the popover
- Clicking outside the popover closes it (unless using
triggerType="hover") - Returns focus to the trigger element when closed
- Close button has accessible label via
closeLabelprop - Popover is labeled via title slot or
aria-labelwhen no title is provided - Optional focus trapping when
trapFocusis enabled - Supports keyboard navigation with Tab and Shift+Tab
Best Practices
- Always provide a trigger element for keyboard and screen reader users
- Use the
titleslot to provide context about the popover content - For hover-triggered popovers, ensure content is also accessible via click or focus
- Keep popover content concise and focused
- Use
closeLabelto provide clear close button instructions for screen readers - Avoid nesting interactive elements that might trap focus unexpectedly
- Consider using
matchTriggerWidthfor dropdown-style popovers - For critical actions, use
triggerType="click"to prevent accidental triggers
When to Use Popover vs Tooltip
Use Popover when:
- You need to display rich, interactive content (forms, buttons, lists)
- Content is complex or requires user interaction
- You want to include formatted text, images, or multiple sections
- The content is important enough to require explicit user action to dismiss
Use Tooltip when:
- You need to show brief, non-interactive hints or labels
- Content is simple text (1-2 sentences maximum)
- Information is supplementary and not critical
- You want automatic show/hide on hover without requiring clicks
Positioning with Floating UI
The Popover uses @floating-ui/dom for intelligent positioning:
- Automatic overflow prevention: Popover flips to the opposite side if there's not enough space
- Viewport awareness: Shifts position to stay within the viewport bounds
- Smart arrow positioning: Arrow automatically adjusts based on popover position
- Distance and skidding: Fine-tune positioning with
distanceandskiddingprops - 12 placement options: Comprehensive control over popover positioning
Placement Options
The placement prop accepts these values:
- Basic:
top,right,bottom,left - Start aligned:
top-start,right-start,bottom-start,left-start - End aligned:
top-end,right-end,bottom-end,left-end
<VuePopover placement="right-start">
</VuePopover>