A small glass panel anchored to the button that opens it, such as a short explanation of the data or a few settings, which leaves the map and the rest of the screen usable while it is open and gives focus back to its button when it closes.
When to use
Use Popover for a short explanation or a few settings, such as "About the data", in a small panel against the button that opens it, which leaves the map and the rest of the screen usable.
| Use instead | When |
|---|---|
| Dialog | A question must be answered, or a long form filled, before the person goes on: it holds focus until it closes |
| AlertDialog | A short question, such as "Clear all filters?", is answered by one of its actions |
| Menu | The button opens a list of actions and settings, moved through with the arrow keys |
| Tooltip | A short visual label names a control while a pointer rests on it or the keyboard focuses it |
| MapTooltip | A few words show while the pointer rests on a vehicle or a stop on the map |
| Sheet | The content is a whole panel beside the map, such as a stop's details |
Kongens Nytorv
Metro M1, M2 and M3.
Props
| Name | Type | Default | Description |
|---|---|---|---|
title* | ReactNode | — | The popover's title, in sentence case, such as "About the data". Required: it names the popover for screen readers. |
description | ReactNode | — | One sentence under the title that says what the popover is for, read out with its name when it opens. Phrasing content only: it renders in a <p>. |
children | ReactNode | — | The popover's body, under the description: a few sentences or controls. |
trigger | ReactElement<unknown, JSXElementConstructor<any> | string> | — | The element that opens the popover, such as a Button, rendered in place of the popover; the popover sits against it, and focus returns to it when the popover closes. Leave it out for a popover opened only through open, placed by slotProps.positioner.anchor. |
open | boolean | — | Whether the popover is open. Set it to control the popover, with onClose to hear when it asks to close; leave it out for a popover its trigger opens. |
defaultOpen | boolean | false | Whether the popover is open at first, when open is left out. |
onOpen | (event: Event, details: Popover.Root.ChangeEventDetails) => void | — | Called when the popover asks to open, from its trigger. details is Base UI's, with its reason and cancel(). |
onClose | (event: Event, reason: Popover.Root.ChangeEventReason, details: Popover.Root.ChangeEventDetails) => void | — | Called when the popover asks to close: reason is 'escape-key', 'outside-press', 'close-press', 'focus-out' or another of Base UI's reasons, and details.cancel() keeps it open. |
className | string | — | Reaches the root, the popup. className is merged after pn-Popover. |
style | CSSProperties | — | Reaches the root, the popup. className is merged after pn-Popover. |
ref | Ref<HTMLDivElement> | — | Reaches the root, the popup. className is merged after pn-Popover. |
closeLabel | string | The locale's | Overrides "Close", the close button's name. |
slotProps | { trigger?: Props<unknown> | undefined; portal?: PopoverPortalProps | undefined; positioner?: PopoverPositionerProps | undefined; title?: PopoverTitleProps | undefined; description?: PopoverDescriptionProps | undefined; close?: Omit<PopoverCloseProps, "disabled"> | undefined; } | — | Props for each part: trigger, portal, positioner, title, description, close. |
title (required)ReactNode, default —- The popover's title, in sentence case, such as "About the data". Required: it names the popover for screen readers.
descriptionReactNode, default —- One sentence under the title that says what the popover is for, read out with its name when it opens. Phrasing content only: it renders in a
<p>. childrenReactNode, default —- The popover's body, under the description: a few sentences or controls.
triggerReactElement<unknown, JSXElementConstructor<any> | string>, default —- The element that opens the popover, such as a Button, rendered in place of the popover; the popover sits against it, and focus returns to it when the popover closes. Leave it out for a popover opened only through
open, placed byslotProps.positioner.anchor. openboolean, default —- Whether the popover is open. Set it to control the popover, with
onCloseto hear when it asks to close; leave it out for a popover its trigger opens. defaultOpenboolean, defaultfalse- Whether the popover is open at first, when
openis left out. onOpen(event: Event, details: Popover.Root.ChangeEventDetails) => void, default —- Called when the popover asks to open, from its trigger.
detailsis Base UI's, with itsreasonandcancel(). onClose(event: Event, reason: Popover.Root.ChangeEventReason, details: Popover.Root.ChangeEventDetails) => void, default —- Called when the popover asks to close:
reasonis'escape-key','outside-press','close-press','focus-out'or another of Base UI's reasons, anddetails.cancel()keeps it open. classNamestring, default —- Reaches the root, the popup.
classNameis merged afterpn-Popover. styleCSSProperties, default —- Reaches the root, the popup.
classNameis merged afterpn-Popover. refRef<HTMLDivElement>, default —- Reaches the root, the popup.
classNameis merged afterpn-Popover. closeLabelstring, default The locale's- Overrides "Close", the close button's name.
slotProps{ trigger?: Props<unknown> | undefined; portal?: PopoverPortalProps | undefined; positioner?: PopoverPositionerProps | undefined; title?: PopoverTitleProps | undefined; description?: PopoverDescriptionProps | undefined; close?: Omit<PopoverCloseProps, "disabled"> | undefined; }, default —- Props for each part:
trigger,portal,positioner,title,description,close.
Use it
import { Popover } from '@mylder/perron'Ask your agent
get_reference({ library: "base-ui", component: "Popover" })Keyboard and ARIA
From the Base UI reference for Popover and the installed PopoverRoot, PopoverTrigger, PopoverPositioner, PopoverPopup, PopoverClose and the focus manager they build on. "From" says who does it.
| Key | Does | From |
|---|---|---|
Enter, Space on the trigger | Open the popover, calling onOpen with reason 'trigger-press', and move focus to the first tabbable element in the popup, the close button, unless initialFocus says otherwise | The browser activates the button; Base UI opens and moves focus |
Tab | Moves focus through the popup's tabbable elements in reading order, the close button, then the content's; from the last, on to the element after the trigger, closing the popover with 'focus-out' | Base UI |
Shift+Tab | Moves focus back through the popup's tabbable elements; from the first, back to the trigger, leaving the popover open | Base UI |
Escape | Closes the popover, calling onClose with reason 'escape-key', and returns focus to the trigger | Base UI |
Enter, Space on the close button | Close the popover, calling onClose with reason 'close-press', and return focus to the trigger | The browser activates the button; Base UI closes |
A press outside the popup and the trigger closes the popover too, with reason 'outside-press', and leaves focus where the press put it.
| Element | Role | Accessible name from | States announced |
|---|---|---|---|
trigger | button, native, or the caller's element's own | Its own content, from the caller | aria-haspopup="dialog", aria-expanded, from Base UI |
positioner | presentation, from Base UI | none | none |
root | dialog, from Base UI | aria-labelledby the title; described by the description through aria-describedby | none: it is not modal, so nothing outside it is hidden |
title | heading, level 2, from the native <h2> | Its text, the caller's title | none |
close | button, native | The owned "Close", or closeLabel, in the CloseButton's VisuallyHidden span | none: it is never disabled (CR-6) |
header, content | none: plain <div>s | none | none |
What axe won't catch, from the contract, each with how this component meets it:
- Everything the eye is told, the ear is told, for every shape each prop accepts: the popover is announced with its
titleas its name and itsdescription, the close button'saria-hiddenicon has the owned "Close" as its text, and that the popover belongs to its trigger is told by the trigger'saria-expandedandaria-controls. - Every landmark and named region has an accessible name: the popover is named by its own heading, the
title, througharia-labelledby, andtitleis required. - Every default label can be overridden:
closeLabel. - Every shape the type accepts renders, in every variant:
title,descriptionandchildrenrender a string, an element and a list alike; a long word in the title, the description or the content wraps (CR-9). - A hidden copy kept for animation is
aria-hidden, and a collapsed region isinert: none here; the closed popover renders nothing, and Base UI makes the positionerinertwhile it closes. - Native semantics first: Base UI's
dialogrole, a native<h2>for the title, native<button>s for the trigger and the close button, and no ARIA of Perron's own. - Focus moves in reading order and returns on close: the header comes first in the DOM, then the description and the content, as on screen; Base UI puts the popup in the tab order right after the trigger; opening moves focus into the popup, and closing by Esc or the close button returns it to the trigger, also for a popover open at first render (CR-5).
- Every target is at least
size.target-minsquare: the close button,size.control(34 px) square. - Loops stop when the system asks for less motion: none here; the popover has no loop and no transition (CR-10).
Owned strings
| Key | English | Override | Seen or heard where |
|---|---|---|---|
close | "Close" | closeLabel | close, heard as the close button's name from the CloseButton's VisuallyHidden span; never seen |
close is the key packages/perron/src/locale/en.ts already has, "A button that closes a dialog, panel or notice", so Popover adds no key. Every other word is the caller's, through title, description, children and trigger.
Do and don't
For a caller using the component, with the reason. Each row is a rule: a must row's Don't is never right, a should row's needs a reason, and Checked by says what catches the Don't in a caller's code.
| Rule | Level | Do | Don't | Checked by |
|---|---|---|---|---|
| DD-1 | must | Give every popover a short title in sentence case, such as "About the data" or "Map layers", which names it | Leave the title empty or hide it: the popover then has no name | review |
| DD-2 | must | Pass the button that opens the popover as trigger, so the popover sits against it, is announced as opening and gives focus back | Open a popover from a button placed beside it with open alone, when a trigger would do: the button loses aria-haspopup and aria-expanded, and the popover has nothing to sit against | review |
| DD-3 | must | Set slotProps.trigger.nativeButton to false when trigger is not a <button>, as Base UI warns | Pass a link as the trigger: a link goes somewhere, and a popover opens over the screen | review |
| DD-4 | should | Keep it to a few sentences or a few controls that leave the map usable | Ask a question that must be answered, or put a long form in it: use a Dialog, which holds focus until it is answered | review |
| DD-5 | must | Keep the description to one sentence, and put lists and controls in children | Put block content, such as a list, in description: it renders in a <p> | review |
| DD-6 | should | Open it on a press, and add slotProps.trigger.openOnHover only beside that, as Accessibility asks of anything shown on hover | Use a popover for a few words on hover over the map: that is a MapTooltip | review |
| DD-7 | must | In a Toolbar, pass a ToolbarButton as trigger, and disable it with slotProps.trigger.disabled: it keeps its place in the arrow-key order, and is announced as disabled | Pass a Button as the trigger in a toolbar, or set disabled on the ToolbarButton itself: the arrow keys skip a Button, which adds a tab stop of its own, and a ToolbarButton disabled on its own looks disabled but is announced as enabled | review |
| DD-8 | must | Leave the close button's label to the locale, or override it with closeLabel | Hide the close button with a className: touch screen readers leave the popover through it | review |
| DD-9 | should | Let the popover take z-index.dialog and Base UI's placement, and change the side through slotProps.positioner | Set a z-index, a position or a transform on the popup: it would fall under another overlay or leave its trigger | review |