Popover

User Interface (UI)

How a popover looks, from its parts and where it opens to what goes in it, its states and its tokens.

Let's start with how a popover looks: its parts, where it opens, what goes in it, its states and the variables you can set.

Anatomy

  1. Trigger — The Button that opens it, with popovertarget. It's yours, and it keeps its own label.
  2. Panel — The popover itself, a <div> in the top layer while it's open, which holds every other part.
  3. Title — A few words on what it holds, like "Filters". It names the popover.
  4. Close button — A Button, named "Close", for people who can't press Escape, like on a phone.
  5. Body — Optional. The content, like a few options, a short form or a hint, which scrolls when it doesn't fit.
  6. Offset — The gap between the trigger and the panel.
  7. Arrow — On by default. A small point on the edge that faces the trigger, hidden from assistive technologies.

Every popover has a panel, a title and a close button, and it always renders right after its trigger. That's what puts its content next in the reading order, so Tab and screen readers reach it straight after the button that opened it (WCAG 1.3.2, 2.4.3). You can see the order of its parts in the DOM contract.

Placement

The placement prop says which side of its trigger the popover opens on, and bottom is the default. start and end follow the reading direction, so start is on the left in English and on the right in Arabic. When there isn't room on that side, like for a button at the bottom of the window, the popover flips to the other side.

A start or end popover with room on neither side, like on a phone, opens below its trigger, or above it. When there's still no room, like on a short screen or at 400% zoom, it shrinks to fit on a side with about half the screen or more, and its body scrolls. So in browsers with CSS anchor positioning, it always stays inside the viewport and never covers its trigger. Older ones, Chrome and Edge before 129, Firefox before 147 and Safari before 26, keep it inside the viewport too, but a tall popover can cover its trigger there.

<Button popoverTarget="placement-top" aria-haspopup="dialog" variant="outline">Top</Button>
<Popover id="placement-top" title="Top" placement="top">
  <p>It opens above the button.</p>
</Popover>
{/* …bottom, start and end */}

We recommend bottom for most popovers, which is where Radix and Base UI open theirs by default too. A popover is something people asked for and are about to use, so it opens where their eyes and their hand go next, below the button, the way a select's list does. That's why it differs from the Tooltip's top, which keeps a hint clear of what's below its trigger. Side placements suit triggers stacked on top of each other, like the buttons of a vertical toolbar.

The popover opens in the top layer, with popover="auto", above every other element on the page, an open Dialog included. That way, no overflow: hidden or z-index around its trigger can clip or cover it, and it can stay right after its trigger in the page, rather than moving to the end of the <body>.

CSS anchor positioning then places it beside its trigger (position-area) and flips it when there's no room (position-try-fallbacks), the same way as the Tooltip. Its last fallbacks, @position-try rules, send a start or end popover below or above its trigger, and cap its height at the room on the side it opens on, as long as that's about half the screen. Browsers that don't have anchor positioning yet get the same side from a few lines of script, which you'll find in place() in the reference implementation, without the cap on its height.

Every popover has an arrow by default, as every tooltip does. It sits on the edge that faces the trigger and points at the trigger's centre, or as close as the rounded corners allow, and moves to the other edge when the popover flips. It shows at a glance which button a popover belongs to, which matters most where several sit close together, as the Tooltip's Placement explains. Where only one trigger could be meant, you can turn it off by setting arrow to false, and set its size with --popover--arrow--size.

Title and content

A popover's content goes in two places: a title at the top, beside the close button, and the body below them. Here's the Filters button from the Button page's Composable section, with its popover:

<Button popoverTarget="filters" aria-haspopup="dialog" variant="outline">Filters</Button>
<Popover id="filters" title="Filters">
  <Checkbox defaultChecked>Open issues only</Checkbox>
  <Checkbox>Assigned to me</Checkbox>
  <Checkbox>Has a pull request</Checkbox>
</Popover>
  • The title says what the popover holds, in a word or two, like "Filters" or "Share link". It's also the popover's name, so it's what screen readers announce as focus moves in, and what voice control users and agents find it by. It often repeats the trigger's label, and that's fine, since it tells people they've arrived where they meant to go.
  • The body is for a few controls or a few sentences, like the options above, a short form or a hint with a link. A popover sits on top of the page without blocking it, so anything that needs people's full attention, or more than a screenful, belongs in a Dialog or on a page of its own.
  • The close button is an icon-only, ghost Button named "Close", which closeLabel translates. Escape and a press outside close a popover too, but not every device has a key to press, and a screen reader on a phone can't press outside the popover without leaving it first.

Toggletips

A toggletip is a small button, often an "i" icon, that shows a hint when it's pressed, which makes it a Popover rather than a Tooltip. Unlike a tooltip, it works on touch screens, and it can hold a link, like the one below. Its arrow matters most here, since the button it points at is small and sits in a line of text:

Billing cycle
<span>Billing cycle</span>
<Button popoverTarget="billing-cycle" aria-haspopup="dialog" label="About billing cycles" variant="ghost" size="sm" iconOnly leading={<InfoIcon />} />
<Popover id="billing-cycle" title="Billing cycles">
  <p>You're billed on the day you signed up, every month. <a href="/billing">How billing works</a></p>
</Popover>

Name the button by what it explains, like "About billing cycles", rather than "Info" or "More", since a page with three of them would otherwise have three buttons that sound the same. Some toggletips announce their text through a live region and keep focus on the button, like Inclusive Components describes. Ours moves focus in, like every Popover, which is what lets it hold a link people can reach.

States

A popover is either closed or open, and it shows which with data-state, so your styles and tests can read it in every browser:

StateSelectorRecommended look
Closed[data-state="closed"]Hidden, by the browser's own styles, but still in the page
Open[data-state="open"], and :popover-openBeside its trigger, in the top layer
Opening@starting-styleA fade and a grow from 96% over 150ms, only when people haven't asked for less motion

:popover-open matches the moment it opens, while data-state follows a moment later, once the browser has fired its toggle event, so animate on :popover-open. Hover, focus and disabled belong to the controls inside a popover and to its trigger, like the Button's focus ring, which show them the same way they do everywhere else. You'll find how each state behaves under Every state.

Tokens

The panel looks like a smaller Dialog, with the same border and background. Its width, padding, corners and shadow differ, and so does its type, with a title at the text's own size, 14px rather than 16px, and an xs close button rather than an sm one. It's 15rem wide, 240px at the default text size, so every popover is the same size whatever it holds, and there's no backdrop behind it to set it apart from the page, so a soft shadow does. Its background is Canvas, the background browsers give a document, which follows color-scheme, so it covers what's behind it in your light and dark themes alike.

Its text reads one theme token, --color--neutral-text, like the Dialog's. The Buttons and Checkboxes inside read their own tokens, so copy tokens.css from the Button into your theme. With our reference tokens, the title and the content come to 14.9:1 on white, the same as in the Dialog. If you change the tokens or the background, make sure to check the same pair.

If you need to tune a popover, you can set its own variables from a class:

VariableDefaultWhat it sets
--popover--width15remHow wide it is, and never wider than the screen less 32px
--popover--padding1remThe space inside the panel
--popover--gap0.75remThe space between the header and the body
--popover--radius0.5remThe corners
--popover--border-colorcolor-mix(in srgb, currentColor 20%, transparent)The panel's border
--popover--backgroundCanvasThe panel's background
--popover--box-shadow0 0.25rem 1rem color-mix(in srgb, #000 15%, transparent)The shadow that lifts it off the page
--popover--offset0.5remThe gap between the trigger and the popover
--popover--arrow--size0.75remThe arrow's width and height, before it's turned, when it has one

The Popover's styles sit in the components cascade layer, so your class wins without any specificity tricks. Classes you pass to the Popover land on the popover itself:

ShareLink.tsx
import styles from "./ShareLink.module.css";

export function ShareLink() {
  return (
    <Popover id="share-link" title="Share link" className={styles.wide}>
      …
    </Popover>
  );
}
ShareLink.module.css
.wide {
  --popover--width: 20rem;
}

UI Checklist

RuleLevelScopeRequirementBasisCheck
popover/contrastMustComponentThe title and the content have a contrast of 4.5:1 against the panelWCAG 1.4.3 (AA)axe color-contrast, contrast checker
popover/edgeShouldComponentA border and a shadow set the panel apart from the page behind itOpen ComponentsReview
popover/top-layerMustComponentIt opens in the top layer, so no overflow or z-index around its trigger clips or covers it, above an open Dialog tooHTMLVisual regression test
popover/flipsMustComponentIt opens beside its trigger, on the side placement names, flips to the other side when there's no room and stays inside the viewport, never covering its trigger where the browser has CSS anchor positioningOpen ComponentsVisual regression test
popover/fallback-positionShouldComponentBrowsers without CSS anchor positioning get the same side from a script and stay inside the viewport, although a tall popover can cover its trigger thereOpen ComponentsUnit test, visual regression test
popover/points-at-triggerMustComponentAn arrow, when there is one, sits on the edge facing the trigger and points at its centre, after a flip tooOpen ComponentsUnit test, visual regression test
popover/fits-screenMustComponentIt's never wider or taller than the screen less 16px on each side, down to 320px wide, and its body scrolls when its content doesn't fitWCAG 1.4.10 (AA)Emulation
popover/resizableMustComponentIt's sized in rem, so it grows with its text, and its title wraps rather than getting cut offOpen Components, beyond WCAG 1.4.4, 1.4.12 (AA)200% zoom
popover/reduced-motionShouldComponentIt only animates as it opens when prefers-reduced-motion is no-preferenceWCAG 2.3.3 (AAA)Emulation
popover/forced-colorsMustComponentThe panel's border stays visible in forced colours mode, which drops its shadowOpen ComponentsEmulation
popover/more-contrastShouldComponentThe panel's border is drawn at full strength when prefers-contrast: more is setOpen ComponentsEmulation
popover/follows-directionShouldComponentstart and end follow the reading direction, and its close button mirrors in right-to-left languagesOpen ComponentsUnit test, review

FAQ

Copyright © 2026