Skip to content
UniAria
Color scheme
Contrast
Color scheme
Contrast

Popover

A popover is a dialog anchored to the element that opens it, such as a picker panel, a filter or a share box. It sits over the page without blocking it. UniAria shows it in the top layer with the Popover API, as a manual popover (popover="manual"), places it next to its trigger, and adds what the browser leaves out: an open state you can control, Escape and outside dismissal, focus on open and close, nesting, and a closing phase for exit animations. Browsers without the Popover API get it in place instead.

The APG has no popover pattern. The trigger follows Disclosure (aria-expanded), and the popover follows Dialog (Modal) (role="dialog", a name, Escape, focus in and back). Overlays covers what Popover shares with Dialog and Tooltip: the layer stack, dismissal and the two positioning modes.

A modal and a non-modal popover
View source
<!doctype html><meta charset="utf-8"><style>/* The page's layout. The popovers' look is @uniaria/styles' default. */
:root {
  font:
    15px/1.5 system-ui,
    sans-serif;
}
body {
  display: grid;
  gap: 1rem;
  justify-items: center;
  margin: 1.5rem;
  background: var(--uni-surface-color);
  color: var(--uni-text-color);
}
.row {
  display: flex;
  gap: 0.5rem;
}
.fields {
  display: flex;
  gap: 0.75rem;
}
.fields > uni-number-field {
  inline-size: 6rem;
}
</style><output id="log">Open one, then press Escape or click elsewhere.</output>
<div class="row">
  <uni-popover non-modal>
    <button data-uni-part="popover-trigger">Share</button>
    <div data-uni="popover">
      <div data-element="Arrow"></div>
      <p data-element="Description">Anyone with the link can view.</p>
      <uni-button id="copy">
        <button data-uni="button" data-variant="primary">Copy link</button>
      </uni-button>
    </div>
  </uni-popover>
  <uni-popover position-area="bottom span-right">
    <button data-uni-part="popover-trigger">Size</button>
    <div data-uni="popover">
      <div data-element="Arrow"></div>
      <h3 data-element="Title">Image size</h3>
      <div class="fields">
        <uni-number-field default-value="1280" min-value="1">
          <div data-uni="number-field">
            <label data-element="Label">Width</label>
            <div data-element="Wrapper"><input data-element="Input" /></div>
          </div>
        </uni-number-field>
        <uni-number-field default-value="720" min-value="1">
          <div data-uni="number-field">
            <label data-element="Label">Height</label>
            <div data-element="Wrapper"><input data-element="Input" /></div>
          </div>
        </uni-number-field>
      </div>
      <button data-element="Close">Apply</button>
    </div>
  </uni-popover>
</div>
<script src="/demo/popover.js"></script><script>const log = document.getElementById('log');
const nameOf = (event) =>
  event.target.querySelector('[data-uni-part="popover-trigger"]').textContent;

document.addEventListener('openchange', (event) => {
  if (event.detail) log.textContent = `${nameOf(event)}: open`;
});
document.addEventListener('dismiss', (event) => {
  log.textContent = `${nameOf(event)}: dismiss, ${event.detail.reason}`;
});
document.getElementById('copy').addEventListener('press', () => {
  log.textContent = 'Share: link copied';
});
</script>

Usage

React

import { popover } from '@uniaria/core/popover';
import { useBehavior } from '@uniaria/react';

export function ImageSize() {
  const size = useBehavior(popover, { positionArea: 'bottom span-right' });
  return (
    <>
      <button {...size.parts.trigger}>Size</button>
      <div {...size.parts.root}>
        <div {...size.parts.arrow} />
        <h3 {...size.parts.title}>Image size</h3>
        <label>
          Width <input type="number" defaultValue={1280} />
        </label>
        <button {...size.parts.close}>Apply</button>
      </div>
    </>
  );
}

Vue

<script setup lang="ts">
import { popover } from '@uniaria/core/popover';
import { useBehavior } from '@uniaria/vue';

const size = useBehavior(popover, { positionArea: 'bottom span-right' });
</script>

<template>
  <button v-bind="size.parts.trigger">Size</button>
  <div v-bind="size.parts.root">
    <div v-bind="size.parts.arrow" />
    <h3 v-bind="size.parts.title">Image size</h3>
    <label>Width <input type="number" value="1280" /></label>
    <button v-bind="size.parts.close">Apply</button>
  </div>
</template>

HTML

<uni-popover position-area="bottom span-right">
  <button data-uni-part="popover-trigger">Size</button>
  <div data-uni="popover">
    <div data-element="Arrow"></div>
    <h3 data-element="Title">Image size</h3>
    <label>Width <input type="number" value="1280" /></label>
    <button data-element="Close">Apply</button>
  </div>
</uni-popover>

<script type="module">
  import '@uniaria/elements/popover';
</script>

The trigger and the root must both be inside <uni-popover>.

Render the root always: it stays hidden while closed, and opening it renders nothing. Keep it right after the trigger in the DOM when you can, or after the trigger’s frame when it has one (see The trigger’s label and extras). UniAria passes the trigger to showPopover() as its source, which puts the popover next to it in the Tab order, but browsers that ignore source follow the DOM. Give the root a margin: it is the gap to the trigger (see Positioning).

Non-modal popovers

By default a popover is modal for the keyboard and assistive technology: Tab wraps inside it, and the rest of the page gets aria-hidden until it closes. It is never modal for the pointer: the page still takes clicks, and its scroll isn’t locked. isNonModal gives the native behavior instead. Tab continues past the popover as if it sat right after its trigger, the page stays exposed, and focus moving elsewhere closes it with reason 'blur'. Use it for content that goes with the page, such as help or a share box, and for lists that keep focus in a field.

React

const share = useBehavior(popover, { isNonModal: true });

Vue

<script setup lang="ts">
const share = useBehavior(popover, { isNonModal: true });
</script>

HTML

<uni-popover non-modal>…</uni-popover>

Keeping it open for some clicks

A press outside closes the popover. To keep it open for some targets, such as a toast or a toolbar that edits what the popover shows, return false from shouldCloseOnInteractOutside. On the element it is a property only.

React

const size = useBehavior(popover, {
  shouldCloseOnInteractOutside: (target) =>
    !target.closest('[data-popover-keep]'),
});

Vue

<script setup lang="ts">
const size = useBehavior(popover, {
  shouldCloseOnInteractOutside: (target) =>
    !target.closest('[data-popover-keep]'),
});
</script>

HTML

document.querySelector('#size').shouldCloseOnInteractOutside = (target) =>
  !target.closest('[data-popover-keep]');

Controlled

Pass isOpen and onOpenChange to open the popover from your own state, or to close it from an action inside, such as a design system’s button that closes its popover after it runs. A dismissal, the trigger and the close part only report the change; the popover stays open until you set isOpen to false. On the element, setting the isOpen property makes it controlled: update it from openchange.

const [isOpen, setOpen] = useState(false);
const filters = useBehavior(popover, { isOpen, onOpenChange: setOpen });

The trigger’s label and extras

The trigger is a Button, and it takes Button’s label, description, frame and extras as four parts of its own. All four sit outside the popover, as the trigger does:

  • triggerLabel and triggerDescription go inside the trigger. While rendered, the label alone names the trigger and the description describes it, so an icon or a hint inside stays out of the name. The label also names the popover in the trigger’s place.
  • triggerFrame goes around the trigger and its extras, and triggerExtras beside the trigger, never inside it: your buttons, badges and counts. Both carry the trigger’s state attributes, and the pointer anywhere over the frame hovers the trigger.

React

import { button } from '@uniaria/core/button';
import { popover } from '@uniaria/core/popover';
import { useBehavior } from '@uniaria/react';

export function FruitPicker(props: { fruit: string; onClear(): void }) {
  const picker = useBehavior(popover, {});
  const clear = useBehavior(button, { onPress: props.onClear });
  return (
    <>
      <div {...picker.parts.triggerFrame}>
        <button {...picker.parts.trigger}>
          <span {...picker.parts.triggerLabel}>Fruit</span>
          <span {...picker.parts.triggerDescription}>{props.fruit}</span>
        </button>
        <span {...picker.parts.triggerExtras}>
          <button
            {...clear.parts.root}
            data-variant="quiet"
            aria-label="Clear Fruit"
          >
            <ClearIcon aria-hidden />
          </button>
        </span>
      </div>
      <div {...picker.parts.root}>…</div>
    </>
  );
}

Vue

<script setup lang="ts">
import { button } from '@uniaria/core/button';
import { popover } from '@uniaria/core/popover';
import { useBehavior } from '@uniaria/vue';

defineProps<{ fruit: string }>();
const emit = defineEmits<{ clear: [] }>();

const picker = useBehavior(popover);
const clear = useBehavior(button, { onPress: () => emit('clear') });
</script>

<template>
  <div v-bind="picker.parts.triggerFrame">
    <button v-bind="picker.parts.trigger">
      <span v-bind="picker.parts.triggerLabel">Fruit</span>
      <span v-bind="picker.parts.triggerDescription">{{ fruit }}</span>
    </button>
    <span v-bind="picker.parts.triggerExtras">
      <button
        v-bind="clear.parts.root"
        data-variant="quiet"
        aria-label="Clear Fruit"
      >
        <ClearIcon aria-hidden="true" />
      </button>
    </span>
  </div>
  <div v-bind="picker.parts.root">…</div>
</template>

HTML

<uni-popover>
  <div data-uni-part="popover-trigger-frame">
    <button data-uni-part="popover-trigger">
      <span data-uni-part="popover-trigger-label">Fruit</span>
      <span data-uni-part="popover-trigger-description">Apple</span>
    </button>
    <span data-uni-part="popover-trigger-extras">
      <uni-button>
        <button data-uni="button" data-variant="quiet" aria-label="Clear Fruit">
          <svg aria-hidden="true">…</svg>
        </button>
      </uni-button>
    </span>
  </div>
  <div data-uni="popover">…</div>
</uni-popover>

<script type="module">
  import '@uniaria/elements/button';
  import '@uniaria/elements/popover';
</script>

Without an anchor part, the popover is placed against the frame, so --anchor-width includes the extras. Each extra is a control of its own: name it in its context (“Clear Fruit”, not “Clear”), and it is a Tab stop after the trigger. A press on an extra while the popover is open is a press outside it: the popover closes with reason 'outside' and the extra acts, in one press, and focus stays on the extra.

Positioning

UniAria places the root against the anchor part, else the triggerFrame, else the trigger. Its options are CSS Anchor Positioning’s own, and the page picks one of two modes.

Script mode, the default, places the popover in every browser from two props:

  • positionArea is a CSS position-area value that puts the popover on one side of the anchor, optionally with the other axis’s center or a span-* keyword. Corners and areas over the anchor aren’t supported: the arrow needs a side.
  • positionTryFallbacks is a CSS position-try-fallbacks value: none, or a list of try tactics (flip-block, flip-inline, flip-start, combined as in CSS) and position-area values. The default, flip-block, flip-inline, tries the opposite side on either axis.
positionArea Places the popover
'bottom' (default) Below the anchor, centered.
'bottom span-right' Below, left edges aligned.
'bottom span-left' Below, right edges aligned.
'top', 'left', 'right' On that side, centered.
'right span-bottom' To the right, top edges aligned.
'block-end span-inline-end' Below, aligned with the anchor’s start edge in the page’s writing direction.

Plain logical keywords follow the page’s writing mode, self- ones the popover’s own, as in CSS.

The gaps are the root’s margins, from your CSS. The margin facing the anchor is the gap, the others keep the popover off the viewport’s edges, and a fallback flips them with the side, as CSS does: margin: 8px 0 keeps the popover 8px from its trigger above or below. On a span-* area a side margin also moves it off the anchor’s edge, as in CSS. UniAria reads the computed margins each time it places the popover, so margins that change with state work. It keeps the last option that fit while it still fits, shifts the popover into the viewport, and checks every frame while it is open, so it follows its trigger as the page scrolls.

CSS mode leaves placing the popover to your CSS anchor positioning. Turn it on once for the page, before overlays open. It applies to every popover and tooltip, and to the lists of Select and Combobox, from the next time one opens:

import { configure } from '@uniaria/core/dom';

configure({ positioning: 'css' });
[data-uni='popover'] {
  inset: auto; /* a popover's own is 0 */
  position-area: bottom span-right;
  position-try-fallbacks: flip-block;
  margin: 8px 0;
}

UniAria then links the root to its anchor (an inline position-anchor, with a generated name added to the anchor’s anchor-name list), reads back the side the browser applied for data-placement and placement, writes the CSS variables and places the arrow. It ignores positionArea and positionTryFallbacks. A browser without CSS anchor positioning leaves the popover where your CSS puts it, and development builds warn.

In both modes the popover closes with reason 'scroll' once its trigger has been scrolled out of view.

Nested popovers

A popover opened from inside another stacks above it:

  • Escape closes only the top one.
  • A press inside the outer popover closes only the inner one. A press outside both closes both, top down.
  • Closing a popover closes the popovers opened from inside it, with reason 'parent'.
  • A press on another popover’s trigger closes this popover and opens that one, in one click.

With a Dialog

A modal Dialog makes everything outside it inert, popovers in the top layer included. A popover used in a dialog must be inside the <dialog> element; development builds warn when one opens outside. A dialog opened from a popover stacks above it and leaves it open, and closing the dialog returns focus into the popover.

Without the Popover API

In browsers without the Popover API (Chrome before 114, Firefox before 125, Safari before 17), the popover opens in place: hidden while closed, position: fixed while open, placed by script mode. Give the root a z-index. An ancestor with a transform, a filter or contain clips and offsets it, as it does any fixed element. Server HTML shows the root in those browsers until UniAria starts.

Anatomy

Part Element Notes
trigger button Optional. A press toggles the popover. A Button: another element gets Button’s role and keys. UniAria writes its id (unless it has one), aria-expanded, and aria-controls while open.
triggerLabel any, inside the trigger; usually span Optional. While rendered, it alone names the trigger, and the popover in the trigger’s place. UniAria writes its id (unless it has one).
triggerDescription any, inside the trigger; usually span Optional. Describes the trigger while rendered. UniAria writes its id (unless it has one).
triggerFrame any; usually div Optional. Around the trigger and its extras. It carries the trigger’s state attributes, and the pointer over it hovers the trigger. Without an anchor part, the popover is placed against it.
triggerExtras any, inside the frame; usually span Optional. Beside the trigger, never inside it: your buttons, badges and counts. It carries the trigger’s state attributes.
anchor any Optional. The popover is placed against it instead of the frame or the trigger.
root any; usually div Hidden while closed. UniAria writes its id (unless it has one), popover="manual", role="dialog", tabindex="-1", aria-labelledby, aria-describedby, data-placement and its position. Its content is yours.
arrow any Optional. UniAria writes aria-hidden="true" and its offset along the edge; your CSS puts it on the side data-placement names.
title any; usually a heading Optional. Names the popover through aria-labelledby.
description any Optional. Linked through aria-describedby while rendered.
close button Optional. A press closes the popover, even when it isn’t dismissable. A Button too.

A part keeps an id you give it, and everything that refers to the part follows it; UniAria writes an id only on a part that has none (see Ids).

The popover’s name comes from labelledBy and a rendered title, else an aria-label on the root, else the trigger, or its triggerLabel while rendered: a popover opened by “Size” is read as “dialog, Size”. Development builds warn when it has none of these. The custom element has no role of its own and never takes focus.

Props

Prop Attribute Type Default Description
isOpen — (property: setting it makes it controlled) boolean — The open state, controlled. Pair it with onOpenChange.
defaultOpen default-open boolean false The starting state, uncontrolled. With server rendering, it opens after hydration.
isNonModal non-modal boolean false Tab continues past it, the page stays exposed to assistive technology, and focus leaving closes it.
isDismissable dismissable boolean true Escape, a press outside and, when non-modal, focus leaving close it. dismissable="false" turns all three off; the trigger, the close part and your code still close it.
isKeyboardDismissDisabled keyboard-dismiss-disabled boolean false Escape alone doesn’t close it.
shouldCloseOnInteractOutside — (property only) (target: Element) => boolean — Return false to keep it open for a press on target.
isDisabled disabled boolean false The trigger can’t open it: Button’s isDisabled on the trigger.
shouldRestoreFocus restore-focus boolean true Focus goes back when it closes (see Accessibility). restore-focus="false" turns it off.
shouldFocusOnOpen focus-on-open boolean true Focus moves into the popover when it opens. Off, focus stays where it is. focus-on-open="false" turns it off.
positionArea position-area PositionArea 'bottom' A CSS position-area on one side of the anchor. Script mode only.
positionTryFallbacks position-try-fallbacks string 'flip-block, flip-inline' A CSS position-try-fallbacks. Script mode only.
labelledBy labelled-by string — More ids for aria-labelledby.
describedBy described-by string — More ids for aria-describedby.
tooltip tooltip (the <uni-tooltip>’s id) a tooltip’s instance — A Tooltip for the trigger. On the element, a <uni-tooltip> that <uni-popover> sits in directly links too. With shouldOpenOnlyWhenTruncated, it measures triggerLabel and triggerDescription, else the trigger.

PositionArea is exported from @uniaria/core/popover as a type.

Events

Event Callback prop Detail When
openchange onOpenChange boolean The trigger, a dismissal or the close part asks to open or close it. Never when you set isOpen.
dismiss onDismiss PopoverDismissEvent The user, or a popover it was opened from, asked to close it, just before openchange with false.
interface PopoverDismissEvent {
  reason:
    | 'escape' // Escape in the popover or on its trigger
    | 'outside' // a press outside
    | 'trigger' // a press on its own trigger
    | 'close' // the close part
    | 'blur' // focus left a non-modal popover
    | 'scroll' // its trigger was scrolled out of view
    | 'parent' // the popover it was opened from closed
    | 'browser'; // it was removed from the page while open
}

A close for 'scroll' leaves focus alone, since moving it would scroll the page back. The custom element dispatches both events on itself, bubbling, with the value in detail.

Actions

Action What it does
open() Opens the popover, as its trigger does, and moves focus inside unless shouldFocusOnOpen is off. isDisabled only disables the trigger, so open() still works.
close() Closes it after its exit animations and returns focus. Reports openChange, but no dismiss: no user asked.

In React and Vue they are on the handle (size.actions.open()); on the custom element they are methods (document.querySelector('uni-popover').open()). While isOpen is controlled, each only reports openChange and leaves the change to you.

State

Field Type Meaning
isOpen boolean The popover is open.
isEntering boolean The first frames after opening, for enter transitions.
isExiting boolean Closed, but still shown until its exit animations end.
placement 'top' | 'bottom' | 'left' | 'right' | undefined The side it was placed on, after fallbacks; undefined while it isn’t shown.
const placement = useBehaviorState(size, (state) => state.placement);

On the custom element, read element.instance.getState(). See State.

Styling

Attribute On When
data-uni="popover" root Always.
data-uni-part="popover-trigger", data-uni-part="popover-anchor" trigger, anchor Always.
data-uni-part="popover-trigger-label", …-trigger-description, …-trigger-frame, …-trigger-extras the trigger’s label, description, frame and extras Always.
data-element every other part Always.
data-open every part but the anchor and the trigger’s label and description The popover is open.
data-entering every part but the anchor and the trigger’s label and description The first frames after opening.
data-exiting every part but the anchor and the trigger’s label and description It is closing, until its exit animations end.
data-placement root, arrow While shown: the side it was placed on, top, bottom, left or right.
data-hovered, data-pressed, data-focused, data-focus-visible, data-disabled trigger, its frame and extras, close As on a Button. The frame and the extras carry the trigger’s, and data-hovered is set while the pointer is anywhere over the frame.
CSS variable On Value
--anchor-width root The anchor’s width, in pixels: min-width: var(--anchor-width) makes the popover at least as wide as its trigger, or its frame with the extras.
--available-width, --available-height root The most room the popover could take in any of its options, in pixels, for max-width and max-height, so a cap never stops a fallback.

Nothing caps the popover’s size unless you use them. A popover’s default overflow: auto clips an arrow outside its box: give the root overflow: visible, and scroll an element inside it instead.

[data-uni='popover'] {
  min-width: var(--anchor-width);
  max-height: var(--available-height);
  margin: 8px 0;
  overflow: visible;
}
[data-uni='popover'] [data-element='Arrow'] {
  position: absolute;
  width: 14px;
  height: 14px;
}
/* Rotate a child of the arrow: UniAria measures the arrow itself. */
[data-uni='popover'] [data-element='Arrow']::before {
  content: '';
  position: absolute;
  inset: 2px;
  background: Canvas;
  rotate: 45deg;
}
[data-element='Arrow'][data-placement='bottom'] {
  top: -8px;
}
[data-element='Arrow'][data-placement='top'] {
  bottom: -8px;
}

UniAria measures the arrow’s box, transforms included, to center it on the anchor. Rotating the arrow part itself would move it off center by a few pixels, so rotate a pseudo-element or a child, as here.

Exit animations work as in Dialog: style the hidden look on [data-entering] and [data-exiting], and UniAria calls hidePopover() once the animations running on the root and inside it end.

Inline styles UniAria writes, and puts back when the popover closes: in script mode, position: fixed and the top, right, bottom and left longhands on the root; in CSS mode, position-anchor on the root and an anchor-name entry on the anchor; in both, the CSS variables and the arrow’s left or top. The trigger and close parts get Button’s touch-action.

Default styles

The optional @uniaria/styles package styles every part, as in the demo above, whose CSS tab holds only the page’s layout. The popover is the surface Select’s popup uses, with its parts in a column 8 pixels apart and 12 pixels in from its edge. Its 8-pixel margin is the gap from the trigger and keeps it off the viewport’s edges, and it is no larger than the room UniAria measures. Without an arrow it scrolls inside; with one it keeps its overflow for the arrow, so scroll an element inside it. The arrow points at the trigger from whichever side the popover opened on. The trigger and the close part look like Button, data-variant included. A trigger with a triggerLabel is laid out as an item: its label and description take one line each and end in an ellipsis, and in a frame the extras sit over the trigger’s end (see Labels, frames and extras). Opening, the popover fades in and grows from 90% from the trigger’s side, in 120 milliseconds.

Keyboard

Key Behavior
Enter, Space On the trigger, toggles the popover. On the close part, closes it.
Escape With focus in the open popover or on its trigger, closes the top popover when it is dismissable and keyboard dismissal isn’t disabled. A widget inside that handles Escape and prevents its default keeps it open.
Tab, Shift+Tab In a modal popover, move through its tabbable elements and wrap at both ends.
Tab In a non-modal popover, continues as if the popover sat right after its trigger: from its last element, focus goes to what follows the trigger, and the popover closes.
Shift+Tab In a non-modal popover, from its first element, focus goes to the element before the trigger, and the popover closes.

Each of the trigger’s extras is a Tab stop of its own after the trigger, in DOM order. A modal popover keeps Tab inside it, so the extras are reached once it closes. From a non-modal one, Tab from its last element goes on to the first extra, which follows the trigger, and the popover closes.

Accessibility

  • Focus on open. Focus goes to the first element marked autofocus or data-autofocus, else to the popover itself, so a screen reader reads its name first. With shouldFocusOnOpen off, focus stays where it is.
  • While a modal popover is open, Tab wraps inside it. A press outside that closes the popover lets focus go where the press puts it, and the press acts on what it hit; focus that leaves any other way is brought back.
  • Focus on close, after the exit animation: the trigger, else the element focused before it opened. Focus stays put when it has already moved to an element outside the popover and its trigger: after a click on a field outside, it stays on the field. Natively it would land on the body.
  • The page behind a modal popover gets aria-hidden="true", never inert, so the pointer still works. Live regions stay exposed.
  • Spoken output. “button, Size, not expanded” on the closed trigger; “dialog, Size” when it opens, named by the trigger, or “dialog, Image size” with a title. With a triggerLabel “Fruit” and a triggerDescription “Apple”, the trigger reads “button, Fruit, Apple, not expanded” and the popover “dialog, Fruit”.
  • Extras aren’t part of the trigger’s name or description: each reads on its own, such as “button, Clear Fruit”. Text that should describe the trigger, such as a badge, goes in triggerDescription.
  • Trigger ARIA. aria-expanded always and aria-controls while open, but no aria-haspopup: screen readers read most of its values as “menu”. UniAria doesn’t use popovertarget, which would toggle the popover behind the open state.
  • Touch screen readers can’t press outside a modal popover they’re in. Render a close part, visually hidden if it shouldn’t show.
  • Tooltips inside. A Tooltip’s Escape closes the tooltip first; the next Escape closes the popover.

Compared with other libraries

  • One click outside closes it and acts on what it hit, as the native popover does. React Aria swallows that click, so a second click acts. One click on another popover’s trigger switches popovers.
  • Modal for the keyboard and screen readers only. React Aria’s popover makes the page inert, locks scroll and adds an underlay; Zag’s and the native popover are non-modal, which isNonModal gives.
  • The top layer, no portal, with popover="manual": UniAria owns every close, so a controlled popover can refuse one, and an unrelated popover opening doesn’t close it, as it would with popover="auto".
  • CSS Anchor Positioning’s options and the root’s margins instead of placement, offset, crossOffset and shouldFlip. React Aria’s 'bottom start' is 'bottom span-right'.
  • Scrolling moves it with its trigger and closes it once the trigger leaves view. React Aria closes a non-modal popover on any scroll.
  • A press outside nested popovers closes every popover it is outside of, as the native popover does. React Aria closes only the top one.
  • Extras beside the trigger are outside it: a press on one closes the popover and acts. Cube UI Kit keeps its popup open for its trigger’s actions.