Skip to content

Popover

Experimental Alpha

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

Vue
Lit
React
Live Preview

Basic Popover

Popover Title

This is the popover content. You can put any content here including text, links, buttons, and more.

Button Trigger User Information

Popovers can contain rich content and interactive elements.

Link Trigger Link Popover

Popovers can be triggered from links too.

More Options
Navigation Menu

Trigger Types

Click TriggerClick Popover

This popover opens when you click the button.

Hover Trigger Hover Popover

This popover opens when you hover over the button.

Focus Trigger Focus Popover

This popover opens when the button receives focus (keyboard navigation).

Placement Options

TopTop Placement

Popover positioned above

RightRight Placement

Popover positioned to the right

BottomBottom Placement

Popover positioned below

LeftLeft Placement

Popover positioned to the left

Rich Content

User Profile John Doe
JD
John Doe
john@example.com
View Profile
Add Comment New Comment
Submit

Without Close Button

No Close ButtonTitle Only

This popover has no close button. Click outside to close.

No Arrow Popover Without Arrow

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

Toggle Popover Event Tracking

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.

Gradient Popover Customized Style

This popover has been customized with CSS Shadow Parts for a gradient background!

Success Popover Success Theme

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>
  );
}
Open in StackBlitz

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:

bash
npx ag init --framework FRAMEWORK # react, vue, lit, svelte, etc.
npx ag add Popover

The 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
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
tsx
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)
typescript
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

PropTypeDefaultDescription
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
distancenumber8Distance in pixels between the popover and trigger element
skiddingnumber0Offset in pixels along the alignment axis
arrowbooleantrueWhether to show an arrow pointing to the trigger element
disabledbooleanfalsePrevents the popover from opening
triggerType'click' | 'hover' | 'focus''click'How to trigger the popover (click, hover, or focus)
matchTriggerWidthbooleanfalseMakes the popover's width match the trigger element's width
showCloseButtonbooleantrueWhether to show a close button (×) in the popover header
closeLabelstring'Close popover'Accessible label for the close button (for screen readers)
trapFocusbooleanfalseWhether to trap keyboard focus within the popover when open

Events

EventFrameworkDetailDescription
showVue: @show
React: onShow
Lit: @show or .onShow
{ visible: boolean }Fired when the popover becomes visible. The visible property will be true.
hideVue: @hide
React: onHide
Lit: @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:

  1. addEventListener (Lit/Vanilla JS):
javascript
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);
});
  1. Callback properties (Lit/Vanilla JS):
javascript
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);
};
  1. Framework bindings (Vue/React):
vue
<VuePopover @show="handleShow" @hide="handleHide">
</VuePopover>
tsx
<ReactPopover onShow={handleShow} onHide={handleHide}>
</ReactPopover>

Event Handling Examples

Vue:

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:

tsx
<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:

html
<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.

PartDescription
ag-popoverThe main popover container element that displays the content
ag-popover-arrowThe arrow element that points to the trigger element
ag-popover-closeThe close button element inside the popover header

Customization Example

css
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" and aria-modal="false" for proper screen reader announcement
  • Trigger element has aria-expanded and aria-haspopup attributes
  • 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 closeLabel prop
  • Popover is labeled via title slot or aria-label when no title is provided
  • Optional focus trapping when trapFocus is enabled
  • Supports keyboard navigation with Tab and Shift+Tab

Best Practices

  • Always provide a trigger element for keyboard and screen reader users
  • Use the title slot 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 closeLabel to provide clear close button instructions for screen readers
  • Avoid nesting interactive elements that might trap focus unexpectedly
  • Consider using matchTriggerWidth for 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 distance and skidding props
  • 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
vue
<VuePopover placement="right-start">
</VuePopover>