Popover
The overlay layer new overlays are built from — not a component a screen uses directly (AD + owner 28 Sep 2026: „sjednotit popover do menu-button"). An action menu is Menu button, picking a value is Picker menu (or Select / Record select / Check menu); both sit on this layer. <Popover> measures the anchor, places itself under it (flipping above when there is no room), portals itself (to <body>, or into the scrim of an open modal Dialog) and closes on Escape, on a click outside and when the anchor scrolls away (packages/ui/src/components/Popover.tsx; placement in packages/ui/src/lib/place.ts). The frame — border, shadow, padding, the 60vh scroll cap — is .acrm-popover in apps/web/src/ds.css.
Because it portals, a layer is never clipped by an overflow: hidden ancestor — the reason dropdowns inside dialogs and tables must go through it.
When to use
- Building a NEW overlay component in
packages/uithat has to be anchored to a control (the way Menu button, Picker menu, Record select and Filter chip are built). - Content that must escape a scrolling or clipping container and is not a menu, a picker, a dialog, a tooltip or a preview — rare; ask AD first.
When not to use
- A menu of actions (kebab „⋯", „Přidat aktivitu ⌄") — that is Menu button; its row anatomy lives there.
- Picking one value or several — Picker menu, Select, Record select, Check menu.
- A task with its own actions and a title — a Dialog. A calendar or a longer panel — Picker panel.
- A one-line label for a control — Tooltip. „Who is this?" over a record reference — Entity preview.
- A permanently visible panel; a popover closes when the anchor leaves.
Anatomy
- Panel
.acrm-popover:--surface-100, hairline border,--shadow-pop, paddingvar(--sp-2),z-index: 101,max-height: min(60vh, 420px)withoverflow-y: auto. Width grows to the longest row and never below the anchor; it is clamped only by the viewport edges, and a label longer than the viewport wraps instead of being cut. Nothing in a menu is truncated (AD, 14 Sep 2026; acrm !3532:max-width: calc(100vw - 16px)). An action menu (role=menu, onlymenuitemrows, no field) takes its height from the room in the window instead of the 420px cap (packages/ui/src/lib/fit-height.ts); when it must scroll it ends exactly under the last whole row, so no row (often „Smazat“) is cut. Pickers and listboxes keep the CSS cap and only never overflow the window (acrm !4800). - Scrim
.acrm-popover-scrim(z-index: 85) catches the click outside. - Placement:
align="start"(default) or"end"; vertically 4px under the anchor, flipping above when it would overflow, clamped 8px from the viewport edges. - Rows (
.acrm-popover-item,.acrm-popover-sep,.acrm-popover-empty) are documented with the components that draw them — Menu button (actions: icon, danger, strong, tag, kbd, disabled with reason, empty row) and Picker menu (choice: check). They are not composed by hand on a screen.
Best practices
Do
- Build a new anchored overlay on
<Popover>insidepackages/ui, with a story, and let screens use that component. - Give the panel an
ariaLabelwhen the trigger's own text does not name it. - On a phone, a menu is a BottomSheet — Menu button and Picker menu switch by themselves; a new overlay built here must do the same.
Don't
- Compose an action menu or a picker on a screen from
Popover+.acrm-popover-item— use Menu button / Picker menu (acrm refactor 28 Sep 2026 moved 18 hand-built menus). - A heading over the rows (eyebrow, overline). A menu never has one; the trigger says what this is.
- A second popover opened from a row.
- A hand-built anchored panel:
apps/web/src/lib/anchored-panel.tsis a second copy of the same placement maths; its consumers are moving onto<Popover>and the file is then deleted (AD, OV-4, 14 Sep 2026).
Content
- The layer has no content of its own — the component built on it owns the wording (see Menu button, Picker menu).
Related
menu-button— the action menu (row anatomy),picker-menu— pick one,record-select,select,filter-chip— fields and chips that open this layer.tooltip,entity-preview— the other two anchored layers.- Stories:
components--popover--default,--variants,--states,--with-icon-kbd,--compositiondocument the LAYER (frame, placement, the rows its consumers draw).
Menus inside a dialog on the phone (InSheet, acrm !4333)
- A dialog that is a sheet on the phone provides
InSheetProvider(@acrm/ui,BottomSheet.tsx, read byuseInSheetPanel) around its whole surface — header, body and footer. Every menu opened from it (MenuPanel, PickerMenu, CheckMenu, RecordSelect) renders as an anchored panel.in-sheetinside that sheet, never as a second sheet. - Panel width = sheet width minus
--sp-4on each side (never the trigger width); height capped at 60 % of the visible viewport, content scrolls inside. - When the panel does not fit below its trigger, the dialog grows upwards with the canonical spring (
packages/ui/src/lib/grow-sheet.ts, max 92 %); the body gets the extra room, the footer stays at the bottom, and the dialog returns to its height when the panel closes. Only if 92 % is not enough does the panel get its cap and scroll. - The panel may temporarily cover the dialog footer; it never leaves the sheet.
- Vertical limits come from
visualViewport(place()), so a panel with search stays above the keyboard. - Tap outside closes only the panel, the dialog stays; the tap never falls through to the element below.
- Done (full width, 44 px) only for multi-select (CheckMenu, RecordSelect multi); single choice and actions close on pick.
- Exception: the date calendar (PickerPanel) stays a sheet above the dialog (acrm !4316) — an anchored calendar at 390 px is too narrow.
MenuPanel keepFocus: rows do not take focus, so the selection in a text editor survives (Vložit blok in a note).
Bare Popover is forbidden in pages (acrm !4389, 1. 10. 2026)
- A page never renders
<Popover>directly. Menus = MenuPanel / MenuButton, choices = PickerMenu / CheckMenu, records = RecordSelect (incl. field mode for type-ahead), dates = PickerPanel, colours = ColorPicker.Popoveris the primitive those components are built on. - Enforced by
ds:validaterule bare-popover (tier HARD — fails even without--strict, AST-based, import aliases counted, no baseline). - An approved exception carries
// ds-ok: bare-popover — <reason>at the call site (8 today, approved by AD: notification bell, global search palette, etc.).
Scrollbars in small surfaces (owner approved 1. 10. 2026, acrm !4393, ACRM-424, verified on dev)
- In small scrolling surfaces — RecordSelect / PickerMenu / CheckMenu list, MenuPanel, dialog body, bottom sheet, detail rail (
.sv-fill > .sv-aside) — the scrollbar is NOT drawn on any OS. Content width never changes (row 163/163 px with a long or short list). Scrolling works: wheel, trackpad, touch, keyboard. - That more content follows is shown by a soft fade at the edge where it continues:
data-fade="top|bottom|top bottom"set bylib/scroll-fade.ts(installed once in main.tsx) only when there is overflow on that side; mask on unframed scrollers, a sticky strip in the surface colour on framed panels (popover, sheet), above the sheet footer. - Surface list lives in ONE place (
ds/scrollbar.css=SCROLL_FADE_SELECTOR, contract testlib/scroll-fade.test.ts). Large surfaces (board, tables, lists, page body) keep the canonical thin bar.acrm-scroll(ACRM-132).
Source: docs/popover/popover.usage.md · GitLab