Skip to content
ShineUI
Install

Popover

Rich content on demand.

<sui-popover>OverlaysStable

About

Floating panel anchored to a trigger, for rich content such as filters, a mini profile or an explanation. A click trigger toggles it; trigger-on="hover" turns it into a hover card that also opens on keyboard focus. Reach for it when a tooltip is too small and a dialog too much, and with flush to open a sui-menu.

Usage

<sui-popover width="280"> <sui-button slot="trigger" variant="outline" label="Aging" icon="fa-solid fa-chevron-down" icon-pos="end"></sui-button> <div style="font-size:12.5px;line-height:1.7"> <div style="display:flex;justify-content:space-between"><span>Current</span><b style="font-feature-settings:'tnum'">$18,240.00</b></div> <div style="display:flex;justify-content:space-between"><span>1 to 30 days</span><b style="font-feature-settings:'tnum'">$6,420.00</b></div> <div style="display:flex;justify-content:space-between"><span>31 to 60 days</span><b style="font-feature-settings:'tnum'">$12,480.00</b></div> </div> </sui-popover> <script type="module"> import { defineCustomElements } from "@shineui/core/loader"; import "@shineui/core/shineui.css"; defineCustomElements(); const el = document.querySelector("sui-popover"); el.addEventListener("suiClose", (e) => console.log(e.detail)); </script>
import { SuiPopover } from "@shineui/react"; import "@shineui/core/shineui.css"; export function Example() { return ( <SuiPopover width="280" onSuiClose={(e) => console.log(e.detail)}> <sui-button slot="trigger" variant="outline" label="Aging" icon="fa-solid fa-chevron-down" icon-pos="end"></sui-button> <div style="font-size:12.5px;line-height:1.7"> <div style="display:flex;justify-content:space-between"><span>Current</span><b style="font-feature-settings:'tnum'">$18,240.00</b></div> <div style="display:flex;justify-content:space-between"><span>1 to 30 days</span><b style="font-feature-settings:'tnum'">$6,420.00</b></div> <div style="display:flex;justify-content:space-between"><span>31 to 60 days</span><b style="font-feature-settings:'tnum'">$12,480.00</b></div> </div> </SuiPopover> ); }
<script setup> import { SuiPopover } from "@shineui/vue"; import "@shineui/core/shineui.css"; const onChange = (e) => console.log(e.detail); </script> <template> <SuiPopover width="280" @sui-close="onChange"><sui-button slot="trigger" variant="outline" label="Aging" icon="fa-solid fa-chevron-down" icon-pos="end"></sui-button> <div style="font-size:12.5px;line-height:1.7"> <div style="display:flex;justify-content:space-between"><span>Current</span><b style="font-feature-settings:'tnum'">$18,240.00</b></div> <div style="display:flex;justify-content:space-between"><span>1 to 30 days</span><b style="font-feature-settings:'tnum'">$6,420.00</b></div> <div style="display:flex;justify-content:space-between"><span>31 to 60 days</span><b style="font-feature-settings:'tnum'">$12,480.00</b></div> </div></SuiPopover> </template>
import { Component } from "@angular/core"; import { SuiPopover } from "@shineui/angular"; @Component({ standalone: true, imports: [SuiPopover], template: `<sui-popover width="280" [(open)]="open" (suiClose)="onChange($event)"><sui-button slot="trigger" variant="outline" label="Aging" icon="fa-solid fa-chevron-down" icon-pos="end"></sui-button> <div style="font-size:12.5px;line-height:1.7"> <div style="display:flex;justify-content:space-between"><span>Current</span><b style="font-feature-settings:'tnum'">$18,240.00</b></div> <div style="display:flex;justify-content:space-between"><span>1 to 30 days</span><b style="font-feature-settings:'tnum'">$6,420.00</b></div> <div style="display:flex;justify-content:space-between"><span>31 to 60 days</span><b style="font-feature-settings:'tnum'">$12,480.00</b></div> </div></sui-popover>`, }) export class Example { open = false; onChange(e: CustomEvent) { console.log(e.detail); } }

Basic

The first focusable in the trigger slot gets aria-haspopup="dialog", aria-expanded and aria-controls. A click opens a role="dialog" panel and moves focus onto it, so the role and the content are announced; an outside click closes it.

Current$18,240.00
1 to 30 days$6,420.00
31 to 60 days$12,480.00
<div style="min-height:60px"> <sui-popover width="280"> <sui-button slot="trigger" variant="outline" label="Aging" icon="fa-solid fa-chevron-down" icon-pos="end"></sui-button> <div style="font-size:12.5px;line-height:1.7"> <div style="display:flex;justify-content:space-between"><span>Current</span><b style="font-feature-settings:'tnum'">$18,240.00</b></div> <div style="display:flex;justify-content:space-between"><span>1 to 30 days</span><b style="font-feature-settings:'tnum'">$6,420.00</b></div> <div style="display:flex;justify-content:space-between"><span>31 to 60 days</span><b style="font-feature-settings:'tnum'">$12,480.00</b></div> </div> </sui-popover> </div>

Pinned open

open pins the panel from the host without moving focus. position is the preferred side, and the panel flips to the other one when the viewport has no room. While open the panel paints in the top layer, so a sticky toolbar or a transformed card cannot cover or displace it.

Current$18,240.00
1 to 30 days$6,420.00
31 to 60 days$12,480.00
<div style="min-height:210px"> <sui-popover open width="280"> <sui-button slot="trigger" variant="outline" label="Aging" icon="fa-solid fa-chevron-down" icon-pos="end"></sui-button> <div style="font-size:12.5px;line-height:1.7"> <div style="display:flex;justify-content:space-between"><span>Current</span><b style="font-feature-settings:'tnum'">$18,240.00</b></div> <div style="display:flex;justify-content:space-between"><span>1 to 30 days</span><b style="font-feature-settings:'tnum'">$6,420.00</b></div> <div style="display:flex;justify-content:space-between"><span>31 to 60 days</span><b style="font-feature-settings:'tnum'">$12,480.00</b></div> </div> </sui-popover> </div>

Alignment

align picks the trigger edge the panel lines up with, above or below it. start, the default, and end follow the reading direction, so the panel opens from the leading edge under dir="rtl" too; left and right name sides of the screen in either direction, as position does. The split button takes the same four.

Lined up with the leading edge.
Lined up with the trailing edge.
<div style="min-height:210px;display:flex;justify-content:space-between;gap:20px"> <sui-popover open width="220" align="start"> <sui-button slot="trigger" variant="outline" label="Start" icon="fa-solid fa-chevron-down" icon-pos="end"></sui-button> <div style="font-size:12.5px;color:var(--ink-muted)">Lined up with the leading edge.</div> </sui-popover> <sui-popover open width="220" align="end"> <sui-button slot="trigger" variant="outline" label="End" icon="fa-solid fa-chevron-down" icon-pos="end"></sui-button> <div style="font-size:12.5px;color:var(--ink-muted)">Lined up with the trailing edge.</div> </sui-popover> </div>

Hover cards

A hover card opens after open-delay and hides close-delay after the pointer leaves; keyboard focus opens it at once, so Tab to the link, and a card that holds focus stays up until focus leaves it. hover-persist keeps the second card up until an outside click.

Riverbend Dental Group
Riverbend Dental Group
Open balance $12,480.00. Overdue 15 days.
Riverbend Dental Group
Open balance $12,480.00. Overdue 15 days.
<div style="min-height:200px;display:flex;gap:30px;align-items:center;flex-wrap:wrap"> <sui-popover trigger-on="hover" width="250" open-delay="350" close-delay="200"> <a slot="trigger" href="#" style="font-size:13.5px;font-weight:650;color:var(--link)">Riverbend Dental Group</a> <div style="font-size:12.5px;color:var(--ink-muted);line-height:1.6"><b style="color:var(--ink)">Riverbend Dental Group</b><br>Open balance $12,480.00. Overdue 15 days.</div> </sui-popover> <sui-popover trigger-on="hover" hover-persist width="250"> <button type="button" slot="trigger" style="font:650 13.5px var(--font-sans);color:var(--link);cursor:pointer;background:none;border:0;padding:0">Riverbend Dental Group, persistent</button> <div style="font-size:12.5px;color:var(--ink-muted);line-height:1.6"><b style="color:var(--ink)">Riverbend Dental Group</b><br>Open balance $12,480.00. Overdue 15 days.</div> </sui-popover> </div>

Edge to edge

flush drops the inner padding, so a sui-menu or sui-list runs to the edge of the card. menu makes the trigger a menu button: it announces a menu, opening focuses the first row so the arrows work at once, and the panel has no dialog role. A suiPick listener closes it and focus returns to the trigger.

<div style="min-height:250px"> <sui-popover id="pop-menu" width="230" flush menu> <sui-button slot="trigger" variant="outline" label="Actions" icon="fa-solid fa-ellipsis"></sui-button> <sui-menu id="pop-menu-rows" aria-label="Invoice actions"></sui-menu> </sui-popover> </div>
// setup(root): properties and events setup(root) { const pop = root.querySelector('#pop-menu'); const menu = root.querySelector('#pop-menu-rows'); menu.items = [ { label: 'Edit', icon: 'fa-regular fa-pen-to-square', shortcut: 'E' }, { label: 'Duplicate', icon: 'fa-regular fa-copy' }, { separator: true }, { label: 'Void invoice', icon: 'fa-regular fa-trash-can', danger: true }, ]; menu.addEventListener('suiPick', () => { pop.open = false; }); }

Events

suiOpen fires when a trigger opens the panel, and the cancelable suiClose fires before it closes and says why: escape, outside or trigger, where outside also covers Tab leaving the panel and trigger covers a hover card going away on mouse-out. preventDefault on suiClose keeps the panel open.

Closed.
<div style="min-height:230px"> <sui-popover id="filters" width="260"> <sui-button slot="trigger" variant="outline" label="Filters" icon="fa-solid fa-filter"></sui-button> <div style="display:flex;flex-direction:column;gap:10px"> <sui-checkbox label="Overdue only"></sui-checkbox> <sui-checkbox label="Include drafts"></sui-checkbox> <sui-button variant="brand" size="sm" label="Apply"></sui-button> </div> </sui-popover> <div id="pop-log" style="margin-top:12px;font-size:12px;color:var(--ink-muted)">Closed.</div> </div>
// setup(root): properties and events setup(root) { const pop = root.querySelector('#filters'); const log = root.querySelector('#pop-log'); pop.addEventListener('suiOpen', () => { log.textContent = 'suiOpen.'; }); pop.addEventListener('suiClose', (e) => { log.textContent = 'suiClose (' + e.detail + ').'; }); }

Keyboard

KeyAction
Enter SpaceOn a button trigger, opens or closes a click panel. With menu, opening puts focus on the first row.
ArrowDown ArrowUpOn the trigger of a menu popover, opens it with focus on the first or the last row, and moves focus into an open one the same way. Inside, the arrows are the menu's own.
TabFocus landing on a hover trigger opens its card at once; focus leaving it hides the card after close-delay unless hover-persist is set. Inside a click panel, Tab past the last control closes it and carries on to the control after the trigger; Shift+Tab before the first lands on the trigger.
EscapeCloses the panel and returns focus to the trigger. Inside a dialog the popover takes the key first and the dialog stays; a select list open inside the panel takes it before the popover.

API

<sui-popover>

Properties

PropertyAttributeTypeDefaultDescription
alignalign"end" | "left" | "right" | "start"'start'Which trigger edge the panel lines up with, above or below it; a panel placed to the left or right centers on the trigger instead. start and end follow the reading direction, and start is the default, so the panel opens from the trigger's leading edge under dir="rtl" too; left and right name sides of the screen in either direction, as position does. The same four values as the split button's menuAlign.
closeDelayclose-delaynumber200Grace period in ms before a hover card hides after mouse-out.
flushflushbooleanfalseDrops the panel's inner padding. The panel is padded by default, for prose, fields and cards. Set flush when the panel hosts a list that draws its own row inset, a sui-menu or sui-list, so the rows run edge to edge and the panel is the one and only card around them. Same prop as sui-card.
hoverPersisthover-persistbooleanfalseKeep a hover card open after mouse-out until an outside click.
menumenubooleanfalseThe panel holds a sui-menu, so the trigger is a menu button: it announces a menu (aria-haspopup="menu"), opening it from the trigger by click, Enter, Space or ArrowDown puts focus on the first row, ArrowUp on the last, and the arrows, Home, End and typeahead are the menu's own from there. The panel carries no dialog role, since the menu's role="menu" is the surface a reader hears, and Escape or Tab closes it and hands focus back to the trigger, as on every click panel. Pair it with flush. A hover card ignores it.
openopenbooleanfalseShows the panel (the reference defaultOpen). Mutable; the element opens and closes itself. A panel pinned this way leaves focus where it is; only a trigger gesture moves focus into it. When the panel closes, a popup still open inside it, a select's list or a menu, closes with it and asks through its own suiClose with reason outside.
openDelayopen-delaynumber350Hover open delay in ms. Keyboard focus opens a hover card immediately.
positionposition"bottom" | "left" | "right" | "top"'bottom'Preferred side of the trigger; the panel is position: fixed 8px off it and flips to the opposite side when the viewport has no room.
triggerOntrigger-on"click" | "hover"'click'click (default) toggles on click and opens a role="dialog" panel that takes focus; hover opens a hover card, which also opens on keyboard focus and closes when focus leaves the trigger and the panel together.
widthwidthnumber280Panel width in px.

Events

EventDetailReactVueDescription
suiCloseSuiCloseReason"cancel" | "close-button" | "escape" | "outside" | "pick" | "scrim" | "trigger"onSuiClose@sui-closeFires before the panel closes itself; detail is 'escape' | 'outside' | 'trigger' ('trigger' covers both a trigger click and a hover card's mouse-out or blur; 'outside' covers an outside pointer and focus leaving a click panel on Tab). Cancelable: preventDefault() keeps it open. It does not bubble, so a dialog around the popover never hears it.
suiOpenvoidonSuiOpen@sui-openFires when an interaction opens the panel. It does not bubble.

Slots

SlotDescription
(default)Panel content.
triggerThe element that opens the panel (a sui-button, link or text).

Types

SuiCloseReason typeWhy a dismissible surface is closing. Detail of the cancelable suiClose. A consumer expresses "a stray backdrop click must not close this" by calling preventDefault() on the reason it does not accept, which is why none of these needs a prop of its own.
No members.

Theming

Every design token this element can render with, read from its own stylesheets and the ones it imports. Set a token on :root or on any ancestor and every use below follows it. The values shown are the ones in the theme you are viewing. The list is read per stylesheet, so an element that imports a shared drawing module lists that module's tokens too, whether or not it paints with all of them.

See also