Skip to content
Components, switch product

Popover

<gui-popover> floats a panel of content next to the button that opens it, such as a set of filters or a short form. The panel is your own element with the popover attribute, so the browser does the hard parts: it shows the panel above everything else, closes it on Escape or a click outside, and manages the focus. The element places the panel next to its button and keeps it there.

Code of the example above
PopoverBasic.tsx
import {
GuiButton,
GuiCheckbox,
GuiPopover,
} from '@golemui/gui-components/react';
import '@golemui/gui-components/index.css';
export function PopoverBasic() {
return (
<>
<GuiButton
label="Filters"
variant="outlined"
commandFor="task-filters"
command="toggle-popover"
/>
<GuiPopover>
<div id="task-filters" popover="auto">
<GuiCheckbox label="Assigned to me" value />
<GuiCheckbox label="Due this week" />
<GuiCheckbox label="Show completed" />
</div>
</GuiPopover>
</>
);
}

The element’s entry point is @golemui/gui-components/popover, which registers <gui-popover> and nothing else. To install the package and load the stylesheet, see Installation.

The @golemui/gui-components/react components have typed props and an on* prop for each event. React 18 or later.

PopoverExample.tsx
import { GuiPopover } from '@golemui/gui-components/react';
import '@golemui/gui-components/index.css';
export function PopoverExample() {
return (
<GuiPopover
placement="bottom-start"
>
<div id="task-filters" popover="auto">…</div>
</GuiPopover>
);
}

The panel is the element’s child with the popover attribute, and it needs an id. A button opens and closes it, with no script:

  • A native <button> whose popovertarget names that id. In React, the attribute is the popoverTarget prop.
  • A gui-button with commandfor, the panel’s id, and command="toggle-popover", as in the examples below. In React, they are the commandFor and command props of GuiButton.

The element adds nothing to your markup: the panel, its popover attribute and the button’s popovertarget or commandfor are yours, so a server-rendered page shows the same markup before and after the element upgrades. A closed panel stays hidden until then.

Properties

Property Type Default Description
placement GuiPlacement 'bottom-start' Where the panel sits next to its anchor. Start and end follow the reading direction.
anchor string – The id of the element to anchor the panel to, when it is not the panel's invoker.
open boolean – Whether the panel is open. Without a value it takes the panel's own state. Setting it opens or closes the panel and fires no gui-toggle.
panel read-only, no attribute HTMLElement | null – The app's panel: the element's [popover] child.

Events

Every event bubbles and is composed, so you can listen on any ancestor.

Event Description
gui-toggle The user opened or closed the panel. detail.open is the new state.

placement sets the side of the button the panel opens on: top, bottom, start or end, centered on the button. Add -start or -end to line the panel up with the start or end edge of the button instead, as in bottom-start, the default. Start and end follow the reading direction, so they swap sides in a right-to-left page.

When the panel doesn’t fit on that side of the viewport, it opens on the other side, or with the other alignment. The menu and the tooltip take the same values.

Above the button, centered on it.
After the button, in the reading direction.
Below, lined up with the end of the button.
Code of the example above
PopoverPlacement.tsx
import { GuiButton, GuiPopover } from '@golemui/gui-components/react';
import '@golemui/gui-components/index.css';
export function PopoverPlacement() {
return (
<div className="gui-grid gui-grid--row gui-grid--justify-start">
<div className="gui-grid__cell">
<GuiButton
label="Top"
variant="outlined"
commandFor="placement-top"
command="toggle-popover"
/>
<GuiPopover placement="top">
<div id="placement-top" popover="auto">
Above the button, centered on it.
</div>
</GuiPopover>
</div>
<div className="gui-grid__cell">
<GuiButton
label="End"
variant="outlined"
commandFor="placement-end"
command="toggle-popover"
/>
<GuiPopover placement="end">
<div id="placement-end" popover="auto">
After the button, in the reading direction.
</div>
</GuiPopover>
</div>
<div className="gui-grid__cell">
<GuiButton
label="Bottom end"
variant="outlined"
commandFor="placement-bottom-end"
command="toggle-popover"
/>
<GuiPopover placement="bottom-end">
<div id="placement-bottom-end" popover="auto">
Below, lined up with the end of the button.
</div>
</GuiPopover>
</div>
</div>
);
}
PlacementWhere the panel opens
top, top-start, top-endAbove the button.
bottom, bottom-start, bottom-endBelow the button. bottom-start is the default.
start, start-start, start-endBefore the button, in the reading direction.
end, end-start, end-endAfter the button, in the reading direction.

When the user opens or closes the panel, the element fires gui-toggle with the new state in event.detail.open. Use it to load the panel’s content when it opens, or to save what the user changed in it when it closes.

Ada Lovelace, owner

Grace Hopper, editor

The member list is closed.

Code of the example above
PopoverEvents.tsx
import { useState } from 'react';
import { GuiButton, GuiPopover } from '@golemui/gui-components/react';
import '@golemui/gui-components/index.css';
export function PopoverEvents() {
const [open, setOpen] = useState(false);
return (
<>
<GuiButton
label="Members"
variant="outlined"
commandFor="board-members"
command="toggle-popover"
/>
<GuiPopover
id="members-popover"
onGuiToggle={(event) => setOpen(event.detail.open)}
>
<div id="board-members" popover="auto">
<p>Ada Lovelace, owner</p>
<p>Grace Hopper, editor</p>
</div>
</GuiPopover>
<p id="members-status" role="status">
The member list is {open ? 'open' : 'closed'}.
</p>
</>
);
}

Set open to open or close the panel yourself, such as from a click handler or after a step of a tour. Setting it fires no gui-toggle. When the user opens or closes the panel, open follows, so it always tells the panel’s state. In a framework that holds open in its state, update that state from gui-toggle, or the next time you set it to true nothing changes.

anchor takes the id of the element to place the panel next to. Without it, the panel opens next to the button that opened it, or the first button whose popovertarget or commandfor names it. A panel with nothing to anchor to opens in the middle of the viewport, and the element logs a warning in development.

The panel below has popover="manual": Escape and a click outside don’t close it, so it waits until the user presses “Got it”. A panel with the plain popover attribute closes on both, and closes any other such panel that is open, except the one it opened from.

A panel you open from your code has no button for the browser to return the focus to. When it closes with the focus inside, the element moves the focus to its anchor, where the user can open it again, unless a click outside the panel closed it: the user is pointing somewhere else. Make the anchor an element that takes the focus, such as the native button in this example.

Drag a card to another column to change its status.

Code of the example above
PopoverFromCode.tsx
import { useState } from 'react';
import { GuiButton, GuiPopover } from '@golemui/gui-components/react';
import '@golemui/gui-components/index.css';
export function PopoverFromCode() {
const [open, setOpen] = useState(false);
return (
<>
<button
type="button"
id="board-help"
className="gui-button gui-button--outlined"
onClick={() => setOpen(true)}
>
Show me how
</button>
<GuiPopover
id="board-tip"
anchor="board-help"
open={open}
onGuiToggle={(event) => setOpen(event.detail.open)}
>
<div id="board-tip-panel" popover="manual">
<div className="gui-grid gui-grid--gap-sm">
<p>Drag a card to another column to change its status.</p>
<GuiButton
id="board-tip-close"
label="Got it"
size="sm"
onClick={() => setOpen(false)}
/>
</div>
</div>
</GuiPopover>
</>
);
}

<gui-popover> renders nothing of its own and has no box of its own (display: contents). It places your panel and the stylesheet draws it as a card:

gui-popover
<button type="button" popovertarget="task-filters">Filters</button>
<gui-popover placement="bottom-start" open>
<div id="task-filters" popover>
<!-- Your content. -->
</div>
</gui-popover>

While the panel is open, the element sets its place as inline styles, through the CSSOM: anchor positioning properties where the browser supports them, with an inline anchor-name on the button, or a measured position elsewhere. Leave those properties alone. The element reflects open as an attribute, so your CSS can select gui-popover[open]. See Floating panels for how the panels of the GolemUI Components open above the page.

Select the panel as gui-popover > [popover], or by a class of your own:

styles.css
/* A wider panel with more room, for the popovers of the board toolbar. */
.board-toolbar gui-popover > [popover] {
inline-size: 20rem;
padding: var(--gui-space-4);
}

The panel is never wider or taller than the viewport, less a small gap: longer content scrolls inside it.

The panel reads these design tokens. Set them on :root to change every popover, or on any element to change the popovers inside it:

TokenWhat it styles
--gui-bg-defaultThe background.
--gui-border-defaultThe border.
--gui-radius-mdThe corner radius.
--gui-shadow-mdThe shadow.
--gui-text-defaultThe text.
--gui-space-3The padding.
--gui-overlay-durationHow long the panel takes to fade in and out. Reduced motion skips it.

To restyle beyond the tokens, see Overriding styles; for dark mode and the Clay theme, see Themes and dark mode.

  • Focus. Opening the panel leaves the focus on the button, and Tab from the button moves into the panel. Closing the panel with Escape while the focus is in it returns the focus to the button. A panel you open from your code returns it to its anchor: see Opening from your code.
  • State. The browser tells assistive technology whether the panel of a popovertarget or commandfor button is expanded or collapsed.
  • Role. A popover has no role of its own. Give the panel the role and the name of what it holds, such as role="dialog" and an aria-label for a panel of settings. For a list of actions use gui-menu, and for a short description of a control gui-tooltip.
  • Not modal. The rest of the page stays usable while the panel is open. For content the user must deal with first, use gui-dialog.
KeyAction
Enter SpaceOn the button: opens the panel, or closes it.
TabFrom the button: moves into the panel. From its end: moves on.
EscapeCloses the panel and returns the focus to the button.

What every element shares is in Accessibility.