---
title: "Popover"
description: "Shows content a person can read and operate on a small surface that opens beside the control they pressed and stays with it, without dimming or blocking the rest of the page; in a compact window, in a modal surface with the bottom sheet's appearance."
url: "https://tanrei.chnotchy.com/components/popover/"
last_updated: 2026-10-23T21:17:12.000Z
---

# Popover

> Shows content a person can read and operate on a small surface that opens beside the control they pressed and stays with it, without dimming or blocking the rest of the page; in a compact window, in a modal surface with the bottom sheet's appearance.

## Overview

Use a popover for content a person works with beside the control that opened it: a few settings, a short form, a share panel, a note that needs a link or a button. The rest of the page stays usable around it. In a compact window it takes the bottom sheet’s appearance at the bottom of the window instead, as every popover and dialog of Tanrei does there.

### Usage

The control is yours. On the web you name it with `anchor`, the id of the element the popover stands beside. That element is also the opener: the popover tells assistive technology that it opens a dialog and whether that dialog is open, and gives focus back to it. Where a field anchors the surface and a button inside the field opens it, name the button with `opener`.

A popover is controlled, as a menu is. You set `open` when the opener is pressed and set it back to false when the popover emits `close`. It never opens by itself, on hover, on focus or after a time. `close` carries a reason: `escape`, `outside`, `focus-out`, `close` (its close button) or `anchor-hidden`. As a sheet in a compact window it sends `outside` for a press on its scrim, and never `focus-out` or `anchor-hidden`. An action never closes the popover; your handler does.

Give it a name: `headline` where it draws one, `label` where it does not.

SwiftUI, Compose, Flutter and React Native are planned: the names beside each platform at the top of this page are the ones the spec gives them, and nothing ships there yet.

**Web**

```html
<tanrei-button id="share" variant="outlined">Share</tanrei-button>

<!-- anchor is the id of the control it stands beside. -->
<tanrei-popover id="share-panel" anchor="share" headline="Share this page" closable close-label="Close sharing">
  Anyone with the link can read it.
  <tanrei-button slot="actions" dismissive>Stop sharing</tanrei-button>
  <tanrei-button slot="actions" confirming>Copy link</tanrei-button>
</tanrei-popover>

<!-- A surface under a field: as wide as the field, and focus stays in the field. -->
<tanrei-text-field id="city" label="City"></tanrei-text-field>
<tanrei-popover anchor="city" label="Suggestions" width="anchor" initial-focus="anchor" no-inset>…</tanrei-popover>

<script type="module">
  import '@tanrei/web/components/popover';

  const opener = document.querySelector('#share');
  const panel = document.querySelector('#share-panel');
  // The opener toggles it. A press on the opener is never "outside", so one press never closes and reopens it.
  opener.addEventListener('click', () => (panel.open = !panel.open));
  // Escape, a press outside, focus leaving, the close button, the anchor out of sight.
  panel.addEventListener('close', (e) => {
    console.log(e.detail.reason);
    panel.open = false;
  });
  // An action does not close it: the handler does, when the work is done.
  panel.querySelector('[confirming]').addEventListener('click', async () => {
    await navigator.clipboard.writeText(location.href);
    panel.open = false;
  });
</script>
```

**React**

```tsx
import { useState } from 'react';
import { Button, Popover } from '@tanrei/react';

const [open, setOpen] = useState(false);

<Button id="share" variant="outlined" onClick={() => setOpen(!open)}>Share</Button>
<Popover anchor="share" open={open} headline="Share this page" closable closeLabel="Close sharing" onClose={() => setOpen(false)}>
  Anyone with the link can read it.
  <Button slot="actions" dismissive onClick={stopSharing}>Stop sharing</Button>
  <Button slot="actions" confirming onClick={copyLink}>Copy link</Button>
</Popover>
```

## Specs

### Where it stands

`placement` names a side of the anchor and how the popover lines up along that side. The thirteen values are the tooltip’s, read the same way.

| Value | Where |
| --- | --- |
| `bottom-start` (the default), `bottom`, `bottom-end` | Below the anchor: start edges together, centred, or end edges together |
| `top-start`, `top`, `top-end` | Above the anchor, the same three ways |
| `start`, `start-top`, `start-bottom` | Beside the anchor at its start: centred, top edges together, or bottom edges together |
| `end`, `end-top`, `end-bottom` | Beside the anchor at its end, the same three ways |
| `auto` | The first of bottom, top, end, start that has room |

Start and end follow the layout direction. `offset` is the distance to the anchor in the platform’s layout units, and takes `comp.popover.offset` when you leave it out. `crossOffset` moves the popover along its side, and never off the anchor’s side.

The popover keeps `comp.popover.window-margin` inside the window by these steps, in this order:

1. It flips to the opposite side when the preferred side has no room, keeping the alignment.
2. Where neither side has room, it takes the one with more and is limited to that room. Its content then scrolls.
3. It shifts along its side as far as the margin asks.
4. In a window smaller than its least size, it covers the anchor rather than leave the window.

It follows the anchor when the anchor moves, and keeps the side it is on for as long as that side has room. When no part of the anchor can be seen any more, it emits `close` with the reason `anchor-hidden`.

### Size

A popover is as wide as its content, between `comp.popover.container.min-width` (160) and `container.max-width` (400). With `width="anchor"` it is as wide as the anchor, for a surface that drops from a field. It is never wider than the window less the margins.

It is as tall as its parts, up to `container.max-height` (480) and the room its side has. Beyond that the content scrolls between the header and the actions, which stay. A divider is drawn below the header and another above the actions for as long as the content scrolls. Set `dividerVisibility` to `scroll` (`divider-visibility` on the web) to draw the one below the header only once the content is scrolled, and the one above the actions only while more content lies below; `cupertino` and `m3-like` choose that for every popover. SwiftUI, Compose, Flutter and React Native draw it that way until they catch up.

Set `inset` to false (`no-inset` on the web) for content that pads itself or reaches the edges, such as a list.

### Parts

| Part | What it is |
| --- | --- |
| Header | The headline, and the close button while `closable` is true. It stays in place while the content scrolls |
| Content | The default slot. Anything, including links, fields and other controls |
| Actions | Tanrei Buttons in the `actions` slot, each marked `confirming` or `dismissive` where it is one. They are placed as a Button group places them, and stack when a label no longer fits |
| Arrow | An optional triangle that points at the anchor. `arrow` turns it on for one popover; `comp.popover.arrow.visible` turns it on for a whole design system |

An action without a `variant` takes `comp.popover.actions.variant` (tonal in the default system). The close button is an icon-only small Button in `comp.popover.close-button.variant`: tonal in the default system, as every close button in Tanrei is, so that it reads as a control on any content. The m3-like and cupertino presets keep their bare glyph (text).

### Motion

The popover appears as every surface in Tanrei appears: it fades in and grows a little (from 96 % in the default system) from the middle of the edge that faces the anchor, and leaves the same way back, a little faster. A motion that is interrupted turns round from where it is. Where motion is reduced it only fades. Under the m3-like preset it unfolds from that edge instead, as Material’s menus do. A popover written open in the page’s markup is there from the first frame, without a motion.

### In a compact window

In a window narrower than `comp.popover.compact.window-width` (600 in the default system), a popover takes the bottom sheet’s appearance. This is Tanrei’s rule for every popover and every dialog in a compact window. You still open the same popover with the same props and events; only how it is drawn changes.

- It sits at the bottom edge of the window, as wide as the window up to `comp.bottom-sheet.container.max-width`, in the bottom sheet’s color and elevation. Its top corners take `comp.bottom-sheet.container.shape` and its bottom corners are square. It sits above the on-screen keyboard.
- It is as tall as its parts, and never taller than the window less `comp.bottom-sheet.container.margin-block-start` at the top. Beyond that, the content scrolls between the header and the actions.
- It has no arrow and no drag handle, and it does not follow a drag. `placement`, `offset`, `crossOffset` and `width` do not apply.
- It is modal: the bottom sheet’s scrim dims the page, everything behind it is inert, and focus stays inside it. Escape, back, a press on the scrim and the close button close it.
- It slides up from the bottom edge with the bottom sheet’s motion and slides back down when it closes. Where motion is reduced it fades in place.
- When the window crosses the width while the popover is open, it changes form at once, without motion, and keeps focus where it was.

Set `compactPresentation` to `own` (`compact-presentation="own"` on the web) to keep one popover beside its anchor in every window, for a surface that must stay next to what it belongs to. A popover with `initialFocus="anchor"` always stays beside its anchor, because the field the person is typing in must stay usable. A design system that never wants sheets sets `comp.popover.compact.window-width` to 0.

Menus are not popovers here: a menu, a select’s or a combobox’s list and a button’s menu stay beside their control in every window.

- Category: Containment
- Level: base
- Listing: default
- Page: https://tanrei.chnotchy.com/components/popover/
- Spec: https://github.com/chnotchy/tanrei/blob/main/specs/popover.spec.json

### Availability

| Design system | Has it | Notes |
| --- | --- | --- |
| Tanrei (`tanrei`) | Yes | — |
| Cupertino (Apple HIG-inspired) (`cupertino`) | Yes | — |
| M3 Expressive-like (Material 3 Expressive-inspired) (`m3-expressive-like`) | Yes | — |
| M3-like (Material 3-inspired) (`m3-like`) | Yes | — |

### Platforms

- Web: the element `<tanrei-popover>`, registered by importing `@tanrei/web` (or only its own module, `@tanrei/web/components/popover`).
- React: `Popover` from `@tanrei/react`.

| Platform | Name | Status |
| --- | --- | --- |
| Web | `tanrei-popover` | lagging |
| SwiftUI | `tanreiPopover` | planned |
| Jetpack Compose | `TanreiPopover` | planned |
| Flutter | `TanreiPopover` | planned |
| React Native | `Popover` | planned |

Names a platform gives in its own idiom:

| Spec | Web | SwiftUI | Jetpack Compose | Flutter | React Native |
| --- | --- | --- | --- | --- | --- |
| `open` (prop) | `open` | `isPresented` | `visible` | `controller` | `visible` |
| `label` (prop) | `label` | `accessibilityLabel` | `contentDescription` | `semanticLabel` | `accessibilityLabel` |
| `closable` (prop) | `closable` | `closable` | `closable` | `showCloseButton` | `closable` |
| `closeLabel` (prop) | `closeLabel` | `closeLabel` | `closeLabel` | `closeButtonSemanticLabel` | `closeLabel` |
| `close` (event) | `close` | `onClose` | `onDismissRequest` | `onClose` | `onClose` |

Statuses: `planned` — In the inventory. Nothing shippable exists yet. `lagging` — An implementation of an earlier spec exists; it has not caught up with the current one. `alpha` — Implemented with tests. The API may still change. `beta` — Feature complete and at parity. Only fixes are expected. `stable` — Covered by semantic versioning. `deprecated` — Still shipped; scheduled for removal.

### Anatomy

- `anchor`: The element the popover stands beside: the product's own control (a Button, an icon-only Button, a chip, a field). The popover reads its place, never draws or restyles it. It is also the opener — the element a person presses to open the popover, which carries the open state and takes focus back — unless the product names another element as the opener (a field that anchors the surface, with a button inside it that opens it).
- `scrim` (optional): In a compact window only, while the popover is presented as a sheet: the bottom sheet's scrim over everything behind it (POP-40). It blocks presses and asks the popover to close when pressed.
- `container`: The raised surface that holds everything else, placed on one side of the anchor and aligned along it; in a compact window, with the bottom sheet's appearance at the window's bottom edge (POP-39). It answers no pointer and no press itself and has no states: what is operated is inside it.
- `arrow` (optional): A triangle on the container's edge that faces the anchor, pointing at it; one shape with the container.
- `header` (optional): The row at the top that holds the headline and the close button. It stays in place while the content scrolls.
- `headline` (optional): What the popover is about ("Filters", "Share this page"). It is the popover's accessible name where it is given.
- `close-button` (optional): An icon-only Button of the small size showing the close glyph at the end of the header, in the variant comp.popover.close-button.variant names (tonal in the default system, as every host's close button is). The popover places it and chooses its variant; everything else it reads from Button's own tokens.
- `content`: Whatever the product puts in the popover: text, links, fields, a list, a few settings. The popover insets it, hands it a content color and scrolls it between the header and the actions; it does not lay it out.
- `divider` (optional): Hairlines across the container below the header and above the actions, drawn only while the content scrolls: for as long as it does in the default system, or only while content lies beyond each line under dividerVisibility scroll.
- `actions` (optional): Tanrei Buttons at the foot of the popover, each marked confirming or dismissive where it is one, laid out as a Button group. A Button whose variant the product did not set is tonal in the default system. Optional: a popover whose content acts at once (a setting, a link) needs none.

### Props

| Prop | Type | Default | Web attribute | Description |
| --- | --- | --- | --- | --- |
| `open` | `boolean` | `false` | `open` | Whether the popover is shown. The product opens it when its opener is pressed, and never on hover or on focus. Where open is a plain value (web, Compose, React Native) the product closes the popover by setting it to false when the popover emits close; where it is a binding or a controller (SwiftUI, Flutter) the popover closes itself through it (POP-21). |
| `placement` | `enum`: `top`, `top-start`, `top-end`, `bottom`, `bottom-start`, `bottom-end`, `start`, `start-top`, `start-bottom`, `end`, `end-top`, `end-bottom`, `auto` | `"bottom-start"` | `placement` | Where the popover prefers to stand: a side of the anchor, and how it lines up along that side (POP-2). The thirteen values are the tooltip's, read the same way; the menu's four are among them. top and bottom centre it above or below the anchor; with -start or -end its start or end edge meets the anchor's. start and end centre it beside the anchor; with -top or -bottom its top or bottom edge meets the anchor's. auto takes the first side with room (POP-3). Start and end follow the layout direction. The popover flips to the opposite side when the preferred one has no room, keeping the alignment (POP-5). The default is the menu's: below the anchor, start edges aligned. |
| `offset` | `number` | — | `offset` | The distance between the anchor's edge and the popover, in the platform's layout units; 0 lets them touch. Null takes comp.popover.offset, the system's answer for every popover. Set it only where one anchor's box is not what the eye takes for its edge. |
| `crossOffset` | `number` | `0` | `cross-offset` | How far the popover moves along the side it stands on, in the platform's layout units, from where placement puts it: positive towards the end for a popover above or below, towards the bottom for one beside. It never moves the popover off the anchor's side (POP-4). |
| `arrow` | `boolean` | — | `arrow` | Whether the popover draws an arrow that points at the anchor. Null, the default, takes comp.popover.arrow.visible, the system's choice, which is off in the default system (POP-10). |
| `width` | `enum`: `content`, `anchor` | `"content"` | `width` | What decides the container's width. content: as wide as its content, between comp.popover.container.min-width and comp.popover.container.max-width. anchor: as wide as the anchor, for a surface that drops from a field and should read as its continuation (POP-7). A structural choice drawn by code, not a token axis. |
| `compactPresentation` | `enum`: `sheet`, `own` | `"sheet"` | `compact-presentation` | How the popover is presented in a compact window, one narrower than comp.popover.compact.window-width. sheet: with the bottom sheet's appearance at the window's bottom edge, as a modal dialog (POP-39 to POP-41), as every popover and every dialog of Tanrei is there. own: as a popover in every window, beside its anchor and not modal, narrowing to the window (POP-43), for a surface that must stay beside what it belongs to (a note beside the word it explains). initialFocus anchor keeps the popover's own presentation whatever this says. A structural choice drawn by code, not a token axis. |
| `inset` | `boolean` | `true` | `no-inset` (turns it off) | Insets the content by the container's paddings. Set it to false for content that pads itself or reaches the edges (a list whose rows carry their own padding, an image); the header and the actions keep their insets. |
| `initialFocus` | `enum`: `popover`, `anchor` | `"popover"` | `initial-focus` | Where focus is when the popover has opened. popover: focus moves into it (POP-19), which is right whenever the opener is a button. anchor: focus stays where it is, for a popover that opens under a field the person is typing in, whose keys the field keeps; Tab then moves into the popover (POP-20). It also keeps the popover beside its anchor in a compact window (POP-43): a modal sheet would make the field inert. |
| `headline` | `string` | `""` | `headline` | What the popover is about, drawn in its header and used as its accessible name. Empty draws no headline; the popover is then named by label. |
| `label` | `string` | `""` | `label` | Names the popover for assistive technology where it draws no headline ("Text formatting"). Never drawn. Required while headline is empty; ignored while headline is given. |
| `closable` | `boolean` | `false` | `closable` | Draws the close button at the end of the header. Escape and a press outside already close a popover, so the button is for a popover large enough to cover what a person would press outside it, and for touch, where there is no Escape. |
| `closeLabel` | `string` | `""` | `close-label` | The close button's accessible name ("Close filters"). Required while closable is true. |
| `dividerVisibility` | `enum`: `auto`, `always`, `scroll` | `"auto"` | `divider-visibility` | When the dividers below the header and above the actions are drawn while the content scrolls between them. always: both, for as long as the content scrolls, wherever it is scrolled to. scroll: each only while content lies beyond it — below the header while the content is not at its top, above the actions while more of it lies below. auto takes comp.popover.divider.visibility, the design system's answer (always in the default system). Under every choice a popover whose parts all fit draws none. |

### Events

| Event | Description |
| --- | --- |
| `close` | The popover asks to be closed, every overlay opened from inside it with it: Escape or the platform's back, a press outside, focus leaving it, its close button, or its anchor going out of sight. Carries which of these it was, named escape, back, outside, focus-out, close or anchor-hidden (in the platform's casing). An action never causes it (POP-17). A popover presented as a sheet sends escape, back, outside (a press on its scrim) or close, never focus-out or anchor-hidden (POP-40). |

### Slots

| Slot | Description |
| --- | --- |
| `default` | The content, below the header. A fixture's content string is plain text, which takes sys.typescale.body in the content color (POP-14). |
| `actions` | The action items: Tanrei Buttons, each marked confirming or dismissive where it is one. The popover places them (POP-15) and gives those without a variant its own (POP-16). A fixture fills it through its items. |

### Item: `action`

One action at the foot of the popover: a Tanrei Button, placed by the popover as a Button group places its items and given the popover's variant unless the product set one.

It is a Button.

Names: Web `tanrei-button`, SwiftUI `TanreiButton`, Jetpack Compose `TanreiButton`, Flutter `TanreiButton`, React Native `Button`.

| Prop | Type | Default | Web attribute | Description |
| --- | --- | --- | --- | --- |
| `variant` | `string` | `""` | `variant` | The Button's variant (filled, tonal, elevated, outlined, text, or one a system added). Empty, the default: the popover gives it the variant comp.popover.actions.variant names, tonal in the default system (POP-16). Set it to give one action another look. |
| `disabled` | `boolean` | `false` | `disabled` | As the Button's disabled. A disabled action keeps its place and its share of the width. |
| `confirming` | `boolean` | `false` | — | The action completes what the popover is for (Apply, Save, Share). Placed at the end of a row and at the top of a column (BGR-13, BGR-14). It decides the place only, never the look (BGR-19). |
| `dismissive` | `boolean` | `false` | — | The action backs out (Cancel, Reset). Placed at the start of a row and at the bottom of a column (BGR-13, BGR-14). It decides the place only, never the look (BGR-19). |

| Event | Description |
| --- | --- |
| `click` | The action was activated, as the Button's click. The popover does not close by itself (POP-17). |

| Slot | Description |
| --- | --- |
| `label` | The Button's label, as its default slot. A fixture item's content is this label. |

### States

`enabled`

### Accessibility

- Role: `dialog`
- Labeling: The popover is a dialog that is not modal — a modal one while it is a sheet in a compact window (POP-40) — named by its headline, or by label where it draws none. The headline is also exposed as a heading. The opener keeps its own name and role and exposes that it opens a dialog and whether it is open. The close button is named by closeLabel and each action by its own label; confirming and dismissive are not announced, and the actions are read in the order they are placed (BGR-15). The arrow and the dividers are decorative. The popover is not its anchor's description.
- Minimum target size: yes

| Keys | Action |
| --- | --- |
| Enter or Space on the opener | The product opens the popover; focus moves into it (to the element the product marked, otherwise to the container). Pressed again while it is open, the product closes it. |
| Tab / Shift+Tab | Move through the popover's close button, its content and its actions; the popover comes directly after its opener in the order. Shift+Tab from its first element returns to the opener and leaves it open; Tab from its last element moves on to what follows the opener and closes it. Focus is never kept in. As a sheet in a compact window (POP-40) it keeps focus in: Tab and Shift+Tab wrap. |
| Escape | Closes the popover and returns focus to its opener; with overlays opened from inside it, only the topmost. It goes no further: a dialog the popover stands in stays open. As a sheet, a press on the scrim closes it too. |
| Enter or Space | Activate the focused close button (close, reason close) or action (its click; the popover does not close by itself, POP-17). The popover adds no default action to Enter. |

- The popover has no target of its own; its close button and its actions are Tanrei Buttons, which meet the minimum target themselves (BTN-5). comp.popover.actions.gap keeps each action's whole target in a column in the default system. The opener is the product's control and keeps its own target.
- Tooltip, popover, menu, dialog — which one. A tooltip describes its anchor, opens on hover and focus, never takes focus and holds nothing a person can operate (TIP-13). A popover is opened by a press, takes focus and holds content a person operates. A menu is a list of commands or choices, moved through with the arrow keys; it holds only its items. A dialog stops the task until it is answered. A note that needs a link or a button is a popover, never a tooltip.
- Never open a popover on hover or on focus (POP-18): what appears that way must be dismissible, hoverable and persistent (WCAG 1.4.13) and cannot be reached by touch, and a surface that takes focus because the pointer passed is a trap. Open it from a press.
- Focus: the popover is an operated overlay that is not modal. It moves focus in when it opens and returns it to the opener when it closes, but does not keep it: Tab leaves it, which closes it. An open surface nobody is in would cover content the keyboard user has moved on to, and a pointer user closes it by pressing elsewhere; focus leaving is the keyboard's form of the same thing. This is the menu's rule, the docked picker's and the docked search view's.
- Initial focus goes to the container unless the product marks an element (POP-19), as for the dialog: assistive technology reads the popover's name first, and a field that takes focus by itself would raise the on-screen keyboard over a popover the person has not read. Mark the first field of a short form.
- initialFocus anchor is for a surface under a field the person is typing in (suggestions, a format hint with a link). Focus stays in the field, so the field's own keys keep working; the popover is still next in the Tab order, and still closes when focus leaves both.
- Reading order: the popover follows its opener for a screen reader as it does for Tab (POP-20). On the web, place the element directly after its anchor in the document where the layout allows; where it cannot be, the opener's aria-controls and the focus that moves in carry a person there.
- A press outside closes the popover. Do not put in a popover what a person would lose by one stray press: a long form belongs in a dialog or a sheet. Where open is a plain value the product may also answer close with the reason outside by leaving the popover open; do that only with a visible way to close it (closable, or a dismissive action).
- Keep nesting shallow: an overlay opened from a popover works at any depth (POP-24), but each level is one more thing Escape must be pressed for. One popover inside a popover is the most a product should need; a debug build reports a third.
- Not modal means the rest of the page can still be read and scrolled, and a screen reader's cursor can leave the popover without closing it (POP-21). Nothing a person must answer before going on belongs in a popover.
- Text size and small windows: at 200% text the popover wraps, grows to its limits and scrolls. In a compact window it is a sheet across the window's width (POP-39); one that keeps its own presentation is, at a window 320 wide, the window's width less the margins (POP-43). It never needs scrolling in two directions.
- A compact window (POP-39 to POP-42): there a popover takes the bottom sheet's appearance, as every popover and every dialog of Tanrei does, and becomes a modal dialog — a scrim, everything behind it inert, focus kept in, Escape, back or a press on the scrim to close it. Modal because the sheet form is what the platforms make of a popover on a phone, and they make it modal: iOS adapts a popover to a sheet in a compact size class (UIPopoverPresentationController's adaptive sheet; SwiftUI's presentationCompactAdaptation), and Apple's guidelines present a popover's content in a compact view as a modal sheet; React Spectrum presents a popover as a modal on mobile; Material's bottom sheet that stands in for a menu or a dialog on mobile is a modal one. A surface over a scrim that left the page operable would show one thing and do another (WAI-ARIA APG, dialog: a modal dialog makes what is behind it inert). The opener still says it opens a dialog. Set compactPresentation to own for a popover that must stay beside what it belongs to; initialFocus anchor always keeps it there, because the field being typed in must stay usable.
- Forced colors (POP-32): the container gains an edge in the text color, because its shadow is removed. Contrast in the default system: on-surface on surface-bright is held to 4.5:1 by the token invariants; the container is told from the page by its elevation's ring and shadow, which are not held to a contrast of their own, as for the menu.

### Behaviour

Requirements in EARS form, each with its stable id.

- **POP-1** WHEN open becomes true THE popover SHALL present its container in the platform's overlay layer, the one the menu's surface uses (the web's top layer through the popover API, an overlay SwiftUI draws above its anchor, Compose's Popup, Flutter's Overlay, React Native's overlay layer that TanreiProvider renders), above the other content of its window at comp.popover.container.elevation; WHILE it is open beside its anchor — in every window but a compact one where it is a sheet (POP-39, POP-40) — THE popover SHALL NOT trap focus, SHALL NOT make any other part of the window inert, unscrollable or hidden from assistive technology and SHALL draw no scrim: it is an operated overlay that is not modal. WHEN open becomes false THE popover SHALL remove the container.
- **POP-2** THE popover SHALL read placement as a side and an alignment along that side, as the tooltip reads the same thirteen values (TIP-22): top and bottom SHALL put the container above or below the anchor with its centre on the anchor's, top-start and bottom-start with its start edge on the anchor's start edge, and top-end and bottom-end with its end edge on the anchor's end edge; start and end SHALL put it beside the anchor, on the side the layout direction gives that name, with its centre on the anchor's, start-top and end-top with its top edge on the anchor's top edge, and start-bottom and end-bottom with its bottom edge on the anchor's bottom edge. WHEN placement changes while the popover is shown THE popover SHALL take its new place at once, without a motion, keeping its focus.
- **POP-3** WHILE placement is auto THE popover SHALL try the sides in the order bottom, top, end, start and SHALL take the first that has room for the container within comp.popover.window-margin, or the one with the most room WHEN none has, with its start edge on the anchor's start edge for bottom and top and its top edge on the anchor's top edge for end and start, and SHALL then shift, hold and follow as POP-5 says, keeping the side it took for as long as that side has room.
- **POP-4** THE popover SHALL take the distance between the anchor's edge and the nearest part of itself — the container's edge, or the arrow's tip while an arrow is drawn — from offset, a number of the platform's layout units used as given, so that 0 lets the two touch, and from comp.popover.offset WHILE offset is null; it SHALL then move the container along its side by crossOffset, before the shift of POP-5, and by no more than leaves comp.popover.arrow.width of the container beside the anchor, so that the popover still stands on the anchor's side and an arrow can still reach the anchor. IF offset is negative or not a finite number THE popover SHALL use comp.popover.offset in its place, IF crossOffset is not a finite number THE popover SHALL use 0, and in debug builds it SHALL report the value it ignored.
- **POP-5** THE popover SHALL keep its container at least comp.popover.window-margin inside the window's safe area by these steps, in this order: WHEN the preferred side has no room for the container and the opposite side has, it SHALL flip to the opposite side, keeping the alignment; WHEN neither has room it SHALL use the one of the two with more room and limit the container to that room (POP-7 for its width, POP-8 for its height); it SHALL then shift along the side as far as the margin asks and no farther; and WHERE the container is still larger than the room on its side (a window smaller than the popover's least size) it SHALL move the container back into the margin over the anchor, its top and start edges first, rather than leave the window. It SHALL follow the anchor when the anchor moves (scrolling, resizing, layout), taking these steps again from the side it is on, which it SHALL keep for as long as that side has room, so that a page that scrolls does not throw the popover from side to side. WHEN the anchor is removed, hidden, scrolled out of the window, or scrolled out of sight inside a scrolling ancestor so that no part of it can be seen THE popover SHALL emit close once with the reason anchor-hidden and be closed as POP-21 says, because a surface with nothing beside it no longer says what it belongs to.
- **POP-6** THE popover SHALL fill its container with comp.popover.container.color, raise it by comp.popover.container.elevation, round it by comp.popover.container.shape eased by sys.shape.corner-smoothing (0 is a quarter circle, 0.6 the iOS continuous corner; a platform with only the OS curve draws it for any amount above 0), and clip its header, content and actions to that shape. THE popover SHALL NOT change anything the container draws under a pointer, a press or focus: the container has no hovered, focused or pressed state and draws no state layer.
- **POP-7** WHILE width is content THE popover SHALL make its container as wide as its widest part (its header, its content, or its actions in one row at their own widths) and its paddings, at least comp.popover.container.min-width and at most comp.popover.container.max-width, wrapping content that would be wider; WHILE width is anchor it SHALL make the container as wide as the anchor, and at least comp.popover.container.min-width where the anchor is narrower, comp.popover.container.max-width not applying, and SHALL follow the anchor's width as it changes. In either case the container SHALL never be wider than the window's safe area less comp.popover.window-margin on each side, narrowing below comp.popover.container.min-width where the window asks it to.
- **POP-8** THE popover SHALL make its container as tall as its parts and its paddings, at most comp.popover.container.max-height and at most the room its side leaves within comp.popover.window-margin (POP-5). WHEN its parts do not fit in that height THE popover SHALL keep the header and the actions in place and scroll the content between them, SHALL let keyboard focus and assistive technology scroll it, and SHALL draw a divider of comp.popover.divider.width in comp.popover.divider.color across the container below the header and another above the actions: WHILE its divider visibility (POP-38) is always, both for as long as the content scrolls, wherever it is scrolled to, at its top included; WHILE it is scroll, the one below the header only WHILE the content is scrolled away from its top and the one above the actions only WHILE more of it lies below; WHILE everything fits, or there is no header or no action on that side of the content, THE popover SHALL draw no divider there. WHEN the header and the actions alone leave no room for the content (large text in a short window) THE popover SHALL scroll all of its parts together instead, so that nothing is cut off or unreachable. The dividers SHALL be hidden from assistive technology and SHALL show and hide at once, without motion.
- **POP-9** THE popover SHALL lay out, top to bottom, inside comp.popover.container.padding-block at the top and bottom edges of its container: WHILE headline is not empty or closable is true, the header (POP-11); then the content, comp.popover.container.gap below the header where there is one; then, WHILE the popover has at least one action, the actions, comp.popover.actions.margin-block-start below the content, or below the header where there is no content. WHILE inset is true THE popover SHALL inset the content by comp.popover.container.padding-inline from the container's start and end edges; WHILE inset is false it SHALL let the content reach the container's start and end edges — and its top and bottom edges where no header or no action stands there, the block padding then not applying on that side — clipped by the container's shape, the header and the actions keeping their own insets.
- **POP-10** THE popover SHALL draw an arrow WHILE arrow is true, and WHILE arrow is null and comp.popover.arrow.visible is on, and none otherwise. WHILE it draws one THE popover SHALL draw a triangle comp.popover.arrow.width wide at its base and comp.popover.arrow.height tall on the container's edge that faces the anchor, as one shape with the container — the container's color and elevation, with no seam and no shadow between the two — its tip pointing at the middle of the part of the anchor's edge that the container stands beside, and SHALL keep its base at least comp.popover.arrow.inset, and at least the corner's radius, from each end of that edge, standing at the edge's middle where the edge is too short for both; the container SHALL stand comp.popover.arrow.height farther from the anchor, so that the tip keeps the offset (POP-4). WHEN the container flips, shifts or follows the anchor (POP-5) the arrow SHALL move with it to the edge that now faces the anchor and point at the anchor again, and WHILE the container covers the anchor (POP-5) THE popover SHALL draw no arrow. The arrow SHALL take no press of its own and SHALL be hidden from assistive technology.
- **POP-11** WHILE headline is not empty or closable is true THE popover SHALL draw the header as one row: the headline from comp.popover.container.padding-inline after the container's start edge, taking the width the close button leaves, and, WHILE closable is true, the close button (POP-13) at the row's end, comp.popover.header.gap after the headline and with its edge comp.popover.container.padding-inline before the container's end edge, both aligned to the row's first line; the header SHALL be as tall as the taller of the two and SHALL stay in place while the content scrolls (POP-8).
- **POP-12** WHILE headline is not empty THE popover SHALL draw it in comp.popover.headline.typescale and comp.popover.headline.color, wrapping onto more lines rather than truncating and growing the header with it, and SHALL expose it as a heading. THE popover SHALL set the headline's letter-spacing by the script of its text, as every title is set (PRS-20): no negative letter-spacing WHERE its language is Chinese, Japanese or Korean, and none WHERE its script joins its letters or stacks marks on them.
- **POP-13** WHILE closable is true THE popover SHALL draw at the end of its header an icon-only Button of the small size showing the glyph close of the system's icon set and named by closeLabel, in the variant comp.popover.close-button.variant names, given through the channel a Button reads from the component that places it, and otherwise unrestyled: it reads Button's own tokens, answers hover, focus and press as any Button does and keeps its own minimum target (BTN-5); WHEN the user activates it THE popover SHALL emit close exactly once with the reason close. WHILE closable is true and closeLabel is empty it SHALL still draw the button, and in debug builds report that the button has no accessible name. THE popover SHALL draw no close mark of its own (no path, shape or character of its own).
- **POP-14** THE popover SHALL hand its content comp.popover.content.color as the platform's inherited foreground (the web's inherited color, SwiftUI's foreground style, Compose's content color, Flutter's default text and icon style, React Native's Tanrei text context); plain text given as content, where the platform must style it, SHALL take sys.typescale.body. THE popover SHALL NOT lay out, restyle or mirror the content itself, and SHALL let it hold any element that takes input — links, Buttons, fields, a list, a menu's or another popover's opener.
- **POP-15** WHILE the popover has at least one action THE popover SHALL lay out its actions as a standard Button group lays out its items (BGR-12 to BGR-19), comp.popover.actions.gap apart and comp.popover.container.padding-inline from the container's start and end edges, in the order the group gives them — dismissive at the start and confirming at the end of a row, confirming on top and dismissive at the bottom of a column, unflagged actions between them in the order given — with focus and reading order following that order (BGR-15). WHILE comp.popover.actions.arrangement is fill it SHALL make them a full-width group across the container's inner width, the actions sharing a row equally (BGR-17) or each as wide as the column; WHILE it is end it SHALL keep them at their own widths, the row aligned to the end, and a column as wide as its widest action and aligned to the end. It SHALL place them in a row WHILE every action's label fits its share on one line (fill) or the row at its own widths fits the inner width (end), and otherwise in a column, re-deciding when the container's width or the text size changes.
- **POP-16** WHILE an action's variant is empty THE popover SHALL give that Button the variant comp.popover.actions.variant names, through the channel a Button reads from the component that places it, so that the Button renders from that variant's tokens and states as it would with the variant set; WHILE an action's variant is set THE popover SHALL leave it as set. THE popover SHALL NOT change an action's size or tokens, and SHALL NOT change its look or emphasis for its confirming or dismissive (BGR-19).
- **POP-17** WHEN the user activates an action THE popover SHALL let the Button emit its own click and SHALL NOT close, emit close or otherwise answer it itself, whether the action is confirming, dismissive or neither; the product closes the popover from the action's handler by setting open to false, and focus then returns as POP-22 says.
- **POP-18** THE popover SHALL be shown exactly while open is true: it SHALL NOT open or close itself when a pointer enters or leaves its anchor or its container, when focus arrives at its anchor, on a long-press or after a time, and SHALL have no show or hide delay; the product sets open when the opener is activated by pointer, touch, Enter or Space, and sets it to false when the opener is activated again. THE popover SHALL expose on its opener that it opens a dialog, whether that dialog is open and which element it is, where the platform has those properties (aria-haspopup with the value dialog, aria-expanded and aria-controls on the web), and SHALL NOT change the opener's accessible name, role or look. The opener is the anchor unless the product names another element as the opener, in which case the container is placed against the anchor (POP-2 to POP-5) and everything this spec says of the opener holds for that element.
- **POP-19** WHEN open becomes true WHILE initialFocus is popover THE popover SHALL move focus to the element in it that the product marked for initial focus (the platform's own mark, named in the platform notes) or, without one, to its container, so that assistive technology reads its name first and the next Tab reaches its first focusable element; it SHALL show a focus indicator on the container only when the popover was opened from the keyboard, as sys.behavior.focus-style names. WHEN open becomes true WHILE initialFocus is anchor THE popover SHALL leave focus where it is and SHALL take no key from the element that has it except Escape (POP-23). Focus SHALL move when the popover opens, not when its motion ends; a popover mounted with open true SHALL NOT move focus.
- **POP-20** WHILE the popover is open THE popover SHALL stand in the focus order and in the reading order of assistive technology directly after its opener, wherever the platform draws it: Tab from the opener SHALL move to the popover's first focusable element, Shift+Tab from that element SHALL move back to the opener, Tab and Shift+Tab inside the popover SHALL move through its close button, its content's focusable elements and its actions in the order they are placed, and Tab from its last focusable element SHALL move to the element that follows the opener in the page's order, which closes the popover as focus leaving it does (POP-21). Focus on the opener SHALL count as focus that has not left the popover, so that Shift+Tab to the opener leaves the popover open. THE popover SHALL NOT wrap focus from its last element to its first.
- **POP-21** WHEN the user presses Escape or the platform's back (Android's back gesture or button, the iOS accessibility escape gesture), presses outside the container, its arrow, its anchor, its opener and every overlay opened from inside it (POP-24), moves focus to an element outside all of those, or activates the close button (POP-13), or WHEN the anchor goes out of sight (POP-5) THE popover SHALL emit close once with that reason (escape, back, outside, focus-out, close or anchor-hidden) and SHALL be closed: through its binding or controller where open is one (SwiftUI, Flutter), and by the product setting open to false where open is a plain value (web, Compose, React Native). A press on the opener or the anchor is never a press outside: the product's own handler closes the popover from it (POP-18), so that one press never closes and opens it again. Focus leaving the window (another window or application taking it) SHALL NOT count as focus leaving the popover, and neither SHALL a move of an assistive technology's reading cursor that moves no input focus. Whether a press outside also reaches the element under it SHALL follow the platform and be stated in its README, as for the menu (MNU-5).
- **POP-22** WHEN the popover closes THE popover SHALL return focus to its opener IF focus was inside the popover or an overlay opened from inside it, or was lost with it (a press outside that landed on nothing focusable), and SHALL leave focus where the person put it IF the close came from focus moving to, or a press landing on, another focusable element; WHERE the opener no longer exists or cannot take focus it SHALL fall to the platform's default for a closed overlay. It SHALL show the opener's focus indicator only when the close came from the keyboard. Focus SHALL move when the popover closes, not when its motion ends.
- **POP-23** WHILE the popover is open, WHEN the user presses Escape or the platform's back and neither an overlay opened from inside it nor an element inside it has taken the key for itself (an open menu, a tooltip, a field composing text or clearing its value) THE popover SHALL take the key and close as POP-21 says, and SHALL let it go no further, so that it does not also close a dialog or a sheet the popover stands in; WHILE several overlays are open Escape SHALL close only the topmost.
- **POP-24** WHEN an overlay opens from an element inside the popover's container — a menu, a tooltip, a docked picker or a select's surface, another popover, a dialog or a modal sheet — THE popover SHALL treat it as its own: a press inside it SHALL NOT count as a press outside, focus moving into it SHALL NOT count as focus leaving, and the popover SHALL stay open under it, inert with the rest of the window WHILE that overlay is modal. WHEN that overlay closes THE popover SHALL receive back the focus it returns. WHEN the popover is closed, for a reason POP-21 names or because the product sets open to false, THE popover SHALL close every overlay opened from inside it that is not modal with it at once, their own close events carrying no reason of the person's, and SHALL return focus to its own opener as POP-22 says, not to an opener inside it. WHEN a popover is opened from inside a popover that was itself opened from inside a popover THE popover SHALL still work, and in debug builds SHALL report the depth.
- **POP-25** WHERE the popover's anchor is inside a modal overlay (a dialog, a modal sheet) THE popover SHALL be presented above that overlay and belong to it: operable while the rest of the window is inert, and in that overlay's focus loop directly after its opener (POP-20). WHEN a popover opens while another popover that it was not opened from is open THE popover SHALL be drawn above it; THE popover SHALL NOT close another popover by opening — the press or the focus that opened it closes the other as POP-21 says — so that the product that sets two open at once has two.
- **POP-26** WHEN the popover opens beside its anchor THE popover SHALL make its container appear as every anchored surface appears: from sys.motion.appear.opacity, sys.motion.appear.scale, sys.motion.appear.distance toward the anchor and sys.motion.appear.blur to whole, grown from the middle of the edge that faces the anchor — from its own centre WHERE sys.motion.appear.origin is center, and WHILE it covers its anchor (POP-5) — over comp.popover.motion.open-duration with comp.popover.motion.open-easing, and WHEN it closes THE popover SHALL take it back the same way to where it started, over comp.popover.motion.close-duration with comp.popover.motion.close-easing. WHERE sys.behavior.appearance is unfold the scale SHALL apply along the axis that leaves the anchor only, so that the container unfolds from that edge at its full width. WHERE motion is reduced THE popover SHALL only fade it in and out, over sys.motion.role.reduced, with no scale, distance or blur. WHEN it is mounted with open true THE popover SHALL appear without its motion while the document has not yet been first painted, and with it when it is mounted after that. The motion SHALL be drawn only: the place POP-5 gives and the room the popover is measured for are those of the popover at rest.
- **POP-27** WHEN the popover is told to close while it is still opening, or to open while it is still closing THE popover SHALL turn round from the opacity and the size it has reached, without first jumping to the start or the end of either motion, and SHALL take no longer over the rest than the whole duration of the motion it turns to. WHILE the popover is closed or closing THE popover SHALL take no pointer input and no focus — a press reaches what lies under it — and WHILE it is closed it SHALL take no room in the layout, draw nothing and expose nothing to assistive technology.
- **POP-28** THE popover SHALL expose its container as a dialog named by headline or, WHILE headline is empty, by label: one that is not modal, hiding nothing outside it from assistive technology, WHILE it stands beside its anchor, and a modal dialog WHILE it is presented as a sheet (POP-40); the close button SHALL be named by closeLabel, each action by its own label, and the arrow and the dividers SHALL be hidden. THE popover SHALL NOT expose its text as its anchor's description: what describes an anchor is a tooltip.
- **POP-29** IF headline and label are both empty THE popover SHALL still open, and in debug builds SHALL report that it has no accessible name. IF the popover opens with no content, no headline and no action THE popover SHALL draw nothing, take no focus and expose nothing, and in debug builds SHALL report an empty popover. IF no anchor can be found when open becomes true THE popover SHALL stay closed, emit nothing, and in debug builds report the missing anchor.
- **POP-30** WHILE the text size is enlarged THE popover SHALL keep its width limits (POP-7), wrap its headline and let its content wrap, grow its container's height with them up to the limits of POP-8 and scroll beyond them, stack its actions once a label no longer fits its share (POP-15), and SHALL NOT scale the close button, the arrow, the offset or the window margin beyond what the platform's layout units do. Nothing in the header or the actions SHALL be clipped or truncated.
- **POP-32** WHERE the platform draws with a person's own forced colors (the web's forced-colors mode, and a platform's equivalent where it has one) THE popover SHALL draw an edge sys.border.hairline wide in the platform's text color round its container and its arrow, as one outline, because the mode removes the shadow and may give the container the page's color; its headline and plain text SHALL take the platform's text color.
- **POP-33** THE popover SHALL take its layout direction from where it is used — the direction of the nearest ancestor that sets one, and the platform's own where none does — and SHALL have no direction setting of its own; WHEN that direction changes while the popover is shown THE popover SHALL draw itself for the new direction at once, without a motion of its own, keeping its open state and its focus, the container placed again by the new direction (POP-34).
- **POP-34** WHEN the layout direction is right-to-left THE popover SHALL take the side start as the anchor's right side and the side end as its left, SHALL take the alignments -start and -end of a popover above or below as the right and the left edges, and a positive crossOffset of such a popover as a move towards the left, flipping and shifting from there as POP-5 says; it SHALL leave the sides top and bottom, the alignments -top and -bottom, a crossOffset beside the anchor and the centring on the anchor as they are, and SHALL keep auto's order of sides by their names (POP-3). It SHALL mirror its header — the headline at the right, the close button at the left — apply its paddings by logical start and end, and place its actions as a Button group places them right to left (BGR-8); it SHALL NOT mirror the glyph close, the arrow's shape, or its content, which follows the layout direction itself.
- **POP-35** THE popover SHALL lay out its headline as text of its own direction, isolated from the text around it: it takes the base direction of its first strong character, or the layout direction where it has none, so that a left-to-right word, a number with its sign or unit, a telephone number and an address keep their own order in a right-to-left layout and reorder nothing beside them; THE popover SHALL still place the headline and align its lines by the layout direction, not by the text's own.
- **POP-36** WHILE the on-screen keyboard is shown THE popover SHALL take the keyboard's top edge as an edge of the window's safe area (POP-5), and SHALL keep to the visible part of the window wherever that is smaller than the window: it SHALL flip, shift or shorten so that no part of it is under the keyboard, and SHALL scroll the focused element into the room left; WHEN the keyboard hides it SHALL take its place again by POP-5. It SHALL move as the platform moves content for its keyboard, and at once where motion is reduced.
- **POP-37** WHEN placement, width or initialFocus names a value the popover does not define THE popover SHALL render with the default value, and in debug builds report the unknown value.
- **POP-38** THE popover SHALL draw its dividers (POP-8) as its dividerVisibility names: WHILE dividerVisibility is auto, as comp.popover.divider.visibility names, which follows sys.behavior.divider-visibility unless a design system sets the popover apart; WHILE dividerVisibility is always or scroll, as that value names, whatever the tokens say. WHEN dividerVisibility, or the token it follows, changes THE popover SHALL draw the other form at once, moving no part and keeping the scroll position and focus; WHEN dividerVisibility names a value the popover does not define THE popover SHALL treat it as auto and in debug builds report the unknown value. A platform that cannot draw a choice SHALL fall back to the first value, always, and state that in its README.
- **POP-39** WHILE the window is narrower than comp.popover.compact.window-width (a compact window), compactPresentation is sheet and initialFocus is popover THE popover SHALL present its container with the bottom sheet's appearance (BSH-7) instead of beside its anchor: at the window's bottom edge, as wide as the window but no wider than comp.bottom-sheet.container.max-width and centred in it, filled with comp.bottom-sheet.container.color, raised by comp.bottom-sheet.container.elevation, its top corners rounded by comp.bottom-sheet.container.shape eased by sys.shape.corner-smoothing and its bottom corners square, its color reaching under the bottom system inset and its parts above it, and sitting on the on-screen keyboard while it is shown; as tall as its parts, but never taller than the window below its top system inset less comp.bottom-sheet.container.margin-block-start, its content scrolling beyond that as POP-8 says. It SHALL draw no arrow and no drag handle, follow no drag, close on no swipe, and ignore placement, offset, crossOffset and width there; its header, content and actions SHALL keep their own tokens and layout (POP-9 to POP-17, POP-38) across the sheet's width. WHERE comp.popover.compact.window-width is 0 no window is compact.
- **POP-40** WHILE the popover is presented as a sheet (POP-39) THE popover SHALL be a modal overlay, as a modal bottom sheet is (BSH-2): it SHALL draw comp.bottom-sheet.scrim.color with comp.bottom-sheet.scrim.blur over the whole window behind it (MDL-1), make everything outside it and the overlays opened from inside it inert, keep the content behind it from scrolling, keep focus inside it — Tab and Shift+Tab wrap between its last and first focusable elements — and expose its container as a modal dialog (POP-28). WHEN it opens it SHALL move focus into it as POP-19 says for initialFocus popover — mounted with open true as well, because everything behind it is then inert (as a modal sheet does, BSH-3) — and WHEN it closes it SHALL return focus to its opener as POP-22 says. A press on the scrim SHALL ask to close with the reason outside, Escape with escape (POP-23) and the platform's back with back, and the close button with close (POP-21); since focus cannot leave it and it does not stand beside its anchor, it SHALL NOT ask with focus-out or anchor-hidden and SHALL NOT follow its anchor. Its opener SHALL still say that it opens a dialog and whether it is open (POP-18).
- **POP-41** WHEN the popover opens as a sheet (POP-39) THE popover SHALL slide its container up from below the window's bottom edge into its place while fading the scrim in, as a modal bottom sheet opens (BSH-1), over comp.bottom-sheet.motion.enter-duration with comp.bottom-sheet.motion.enter-easing, and WHEN it closes there it SHALL slide the container down out of view while fading the scrim out, over comp.bottom-sheet.motion.exit-duration with comp.bottom-sheet.motion.exit-easing, turning round from where it has got to as POP-27 says. WHERE motion is reduced it SHALL fade in and out in place without sliding, or appear and disappear at once on a platform whose reduced-motion setting removes every animation (Android's). Focus SHALL move when it opens or closes, not when its motion ends.
- **POP-42** WHEN the window's width crosses comp.popover.compact.window-width, or compactPresentation or initialFocus changes, while the popover is open THE popover SHALL take the other presentation at once, without a motion — beside its anchor and not modal (POP-1 to POP-5), or as a sheet (POP-39, POP-40) — keeping its open state, its scroll position and the element in it that has focus, and SHALL emit nothing; IF focus is outside it (on its opener) as it becomes a sheet THE popover SHALL move focus into it as POP-19 says, because the opener is then inert.
- **POP-43** WHILE compactPresentation is own, or initialFocus is anchor, THE popover SHALL stay an anchored popover that is not modal in every window, compact ones included: WHERE the window is narrower or shorter than the container wants THE popover SHALL narrow and shorten the container as POP-7 and POP-8 say, cover its anchor as POP-5 says where it must, and scroll, and SHALL NOT draw a scrim or trap focus. A popover whose initialFocus is anchor stays beside its anchor because focus stays in the element the person is typing in, which a modal sheet would make inert. WHEN compactPresentation names a value the popover does not define THE popover SHALL treat it as sheet and in debug builds report the unknown value.
- **POP-44** WHERE the platform has ARIA attributes (the web), WHEN the product sets an aria- attribute on the host, by the attribute or by the ARIA property that reflects it, THE popover SHALL move it to the data-aria- attribute of the same name, so that the host is no second node with its name, description or state, and SHALL expose it on its dialog (POP-28), beside its anchor and as a sheet alike — an id reference by the elements it names in the host's tree or a tree around it, a description after its own. WHILE headline or label is not empty it is the dialog's name (POP-28), over a name given so; WHILE both are empty, a name given so is the dialog's name and SHALL NOT be reported as having none in debug builds. WHEN the product removes the aria- attribute or its data-aria- twin, or sets the ARIA property that reflects it (ariaLabel, ariaDescribedByElements…) to null, THE popover SHALL take it off that element.

### Tokens

As written in the token documents (DTCG): an alias in braces names the token it takes its value from, which each design system resolves. On the web each token is a CSS custom property: `--tanrei` and the name after `tanrei`, joined by hyphens (`tanrei.comp.popover.…` → `--tanrei-comp-popover-…`).

#### `tanrei.comp.popover`

| Token | Type | Value |
| --- | --- | --- |
| `comp.popover.container.color` | `color` | `{tanrei.sys.color.surface-bright}` |
| `comp.popover.container.elevation` | `shadow` | `{tanrei.sys.elevation.level2}` |
| `comp.popover.container.shape` | `dimension` | `{tanrei.sys.shape.corner.medium}` |
| `comp.popover.container.padding-inline` | `dimension` | `{tanrei.sys.space.space200}` |
| `comp.popover.container.padding-block` | `dimension` | `{tanrei.sys.space.space200}` |
| `comp.popover.container.gap` | `dimension` | `{tanrei.sys.space.space150}` |
| `comp.popover.container.min-width` | `dimension` | `160px` |
| `comp.popover.container.max-width` | `dimension` | `400px` |
| `comp.popover.container.max-height` | `dimension` | `480px` |
| `comp.popover.offset` | `dimension` | `{tanrei.sys.space.space50}` |
| `comp.popover.window-margin` | `dimension` | `{tanrei.sys.space.space100}` |
| `comp.popover.compact.window-width` | `dimension` | `{tanrei.sys.breakpoint.medium}` |
| `comp.popover.arrow.visible` | `number` | `0` |
| `comp.popover.arrow.width` | `dimension` | `{tanrei.sys.space.space150}` |
| `comp.popover.arrow.height` | `dimension` | `{tanrei.sys.space.space75}` |
| `comp.popover.arrow.inset` | `dimension` | `{tanrei.sys.space.space100}` |
| `comp.popover.header.gap` | `dimension` | `{tanrei.sys.space.space100}` |
| `comp.popover.headline.color` | `color` | `{tanrei.sys.color.on-surface}` |
| `comp.popover.headline.typescale` | `typography` | `{tanrei.sys.typescale.heading-6}` |
| `comp.popover.close-button.variant` | `number` | `1` |
| `comp.popover.content.color` | `color` | `{tanrei.sys.color.on-surface}` |
| `comp.popover.divider.color` | `color` | `{tanrei.sys.color.outline-variant}` |
| `comp.popover.divider.width` | `dimension` | `{tanrei.sys.border.hairline}` |
| `comp.popover.divider.visibility` | `number` | `{tanrei.sys.behavior.divider-visibility}` |
| `comp.popover.actions.gap` | `dimension` | `{tanrei.sys.space.space200}` |
| `comp.popover.actions.margin-block-start` | `dimension` | `{tanrei.sys.space.space200}` |
| `comp.popover.actions.arrangement` | `number` | `0` |
| `comp.popover.actions.variant` | `number` | `1` |
| `comp.popover.motion.open-duration` | `duration` | `{tanrei.sys.motion.role.appear.anchored.enter}` |
| `comp.popover.motion.open-easing` | `cubicBezier` | `{tanrei.sys.motion.easing.enter}` |
| `comp.popover.motion.close-duration` | `duration` | `{tanrei.sys.motion.role.appear.anchored.exit}` |
| `comp.popover.motion.close-easing` | `cubicBezier` | `{tanrei.sys.motion.easing.exit}` |

### What the web draws differently

- The popover is a `popover="manual"` element in the top layer. Inside a modal `<dialog>` it must be a descendant of that dialog to be operable there.
- In a compact window the sheet is a native `<dialog>` opened with `showModal()`, and its scrim is the dialog’s `::backdrop`.
- A press outside closes the popover and also reaches the element under it.
- The web has no back gesture of its own, so `close` never carries the reason `back`.
- `anchorElement` and `openerElement` are properties for an element that an id cannot reach, such as one in another shadow root.
- `arrow` is unset, true or false: the attribute is absent, `arrow`, or `arrow="false"`.
- Mark the element that takes focus on opening with `autofocus`.
- Under forced colors the container and its arrow take an outline in the text color.

## Guidelines

### Tooltip, popover, menu or dialog

| Aspect | Tooltip | Popover | Menu | Dialog |
| --- | --- | --- | --- | --- |
| Opened by | Hover, keyboard focus, long-press | A press on its opener | A press on its anchor | The product |
| Holds | A description of its anchor | Content a person operates | Items only | A decision |
| Takes focus | Never | Yes, and returns it | Yes, and returns it | Yes, and keeps it |
| Blocks the page | No | No (yes as a sheet in a compact window) | No | Yes |

1. If it only says what the control is, and nothing in it can be pressed, it is a tooltip.
2. If it is a list of commands or choices, it is a menu.
3. If the person cannot go on without answering it, it is a dialog.
4. Otherwise it is a popover.

### Do

- Open it from a press on a control, and close it when that control is pressed again.
- Keep the content short. One task, a few fields at most.
- Add `closable` when the popover is large enough to cover what a person would press outside it, and for touch, where there is no Escape.
- Use `width="anchor"` and `initialFocus="anchor"` for a surface under a field the person is typing in.
- Set `compactPresentation` to `own` only for a popover that must stay beside what it belongs to in a compact window, such as a note beside the word it explains.
- Keep action labels short. A label wider than the popover does not wrap.

### Don’t

- Don’t open a popover on hover or on focus. It cannot be reached by touch, and a surface that takes focus because the pointer passed is a trap.
- Don’t put in a popover what a person would lose by one stray press outside it. A long form belongs in a dialog or a sheet.
- Don’t nest more than one popover inside a popover. Each level is one more press of Escape.
- Don’t use a popover for something the person must answer before going on. That is a dialog.

## Accessibility

### What the component handles

- The container is a dialog that is not modal, named by `headline` or by `label`. Nothing outside it is hidden or made inert.
- As a sheet in a compact window, the container is a modal dialog: everything outside it is inert, and Tab and Shift+Tab wrap inside it. Closing returns focus to the opener.
- The opener exposes that it opens a dialog and whether that dialog is open. Its own name and role do not change.
- Opening moves focus to the element you marked, or to the container, so that the popover’s name is read first. The focus indicator shows on the container only when the popover was opened from the keyboard.
- The popover comes directly after its opener in the Tab order, wherever it is in the document. Tab from its last element moves on to what follows the opener, which closes it. Focus is never kept in.
- Escape closes only the topmost overlay, and goes no further: a dialog the popover stands in stays open.
- Closing returns focus to the opener if focus was inside the popover. If the person moved focus elsewhere, it stays there.
- An overlay opened from inside the popover, such as a menu or a tooltip, belongs to it: a press or focus in it does not close the popover.
- At 200% text the popover wraps, grows to its limits and scrolls. In a compact window it is a sheet across the window’s width. A popover that keeps its own presentation is, in a window 320 wide, the window’s width less the margins.

### What you need to do

- Give the popover a `headline` or a `label`, and a `closeLabel` while it is `closable`.
- Set `open` from the opener’s press and set it to false on `close`.
- On the web, place the popover element directly after its opener in the document where you can, so that a screen reader’s reading order follows the Tab order.
- Mark the first field of a short form for initial focus. Leave a popover of text without a mark.
