Repository navigation
feat(docs): add the Popover, with a tested reference implementation - #55
Open
alexgrozav wants to merge 7 commits into
Open
alexgrozav wants to merge 7 commits into
alexgrozav wants to merge 7 commits into
Conversation
The Popover page covers UI, UX, DX and AX, with 37 rules, a contract, live examples in every framework, an anatomy figure, a playground and a tested Vue reference implementation. It's built on popover="auto", opened by a Button with popovertarget, named by its title as a non-modal dialog, and Escape closes only the layer on top, inside a Dialog and around a Tooltip. The Roadmap ticks it, the changelog has an entry, and the Dialog, Tooltip and Button pages link to it. The Button's Filters example now sets aria-haspopup="dialog", which popovertarget doesn't give. Co-authored-by: multica-agent <github@multica.ai>
… its trigger expanded Escape on another control in a Dialog now closes the Popover and leaves the Dialog open. The Popover sets aria-expanded on the trigger that opened it, anchors to that trigger alone, and shrinks to fit when neither side has room. Your code can close and reopen it in one go, and the Vanilla script draws the arrow. New tests cover each, and the stand-in moves focus as browsers do. Co-authored-by: multica-agent <github@multica.ai>
… layer on top The Popover closes when a Dialog around it closes, and hears Escape on the Dialog's parts, after the control that has focus, rather than before it. With a popover open inside it, Escape is the inner one's. Reopening it from another trigger resets the last one, and start and end open below or above a trigger with no room beside it. New tests cover each. Co-authored-by: multica-agent <github@multica.ai>
With focus on the Dialog itself, like after a click on text in the popover, Escape now closes the popover first and the Dialog on the next press. The page says which browsers keep the popover off its trigger, splits the Placement paragraph, and says the popover closes with its Dialog from the keyboard, and with outside from a pointer. Co-authored-by: multica-agent <github@multica.ai>
alexgrozav
marked this pull request as ready for review
October 21, 2026 06:29
alexgrozav
marked this pull request as draft
October 21, 2026 06:38
…y popover
The arrow is on by default, as on the Tooltip, and turns off with
arrow={false}. Every popover is 15rem wide, with the anatomy's title and an
xs close button, so the anatomy, the playground and the examples show the
same panel. --popover--width replaces --popover--max-width.
Co-authored-by: multica-agent <github@multica.ai>
A start or end popover the screen's edge pushes off its trigger's centre now draws its arrow on the edge facing the trigger, in the reference and the Vanilla script. The anatomy follows dark mode, and the Tokens paragraph says the title and close button differ from the Dialog's. Co-authored-by: multica-agent <github@multica.ai>
alexgrozav
marked this pull request as ready for review
October 21, 2026 07:59
alexgrozav
marked this pull request as draft
October 22, 2026 14:30
# Conflicts: # content/docs/3.components/button.md
alexgrozav
marked this pull request as ready for review
October 22, 2026 14:55
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed
This adds the Popover page, across all four layers (UI, UX, DX and AX), with 38 rules that each have a basis, a contract that follows the schema, live examples in every framework, an anatomy figure, a playground and a tested Vue reference implementation in
app/reference/popover/. The Popover is the non-modal sibling of the Dialog and the interactive sibling of the Tooltip, so it keeps the promises those pages already make about it: it opens in the top layer, takes focus, gives it back, andEscapecloses only the layer on top. The page also settles what the Dropdown Menu and the Combobox will reuse from it (placement, light dismiss,Escapeand focus return) without designing them.The decisions a reviewer is most likely to question, with what settled each:
popoverattribute, withautorather thanmanual. The Button page'spopovertarget="filters"opens it with no script, andautogives the top layer, light dismiss,Escape, one open at a time and focus return, all from the platform (HTML Standard, Baseline since 2024).manual, which the Tooltip uses, would mean rebuilding light dismiss andEscapein script, but it would let the Popover ask before closing, as the Dialog does. Flagged:autois our pick, and the price is that it can't ask first (next point).closereports it after the fact. Withauto, the browser closes it on a press outside and onEscapebefore any script hears about it, so unlike the Dialog it can't ask first.closecarries a reason (escape,outside,close-button,triggerorclosed), after the Dialog's. A form whose answers people could lose belongs in a Dialog, and the page says so.role="dialog", named by a requiredtitle. HTML-AAM maps a popover without a role togroup, which isn't announced as something that opened.titleis the Dialog's name for the same job. The title is a<p>, not a heading, since the popover sits in the page's own outline right after its trigger. Flagged: the sources don't settle the element, and a reviewer may prefer a heading.popovertargetandaria-haspopup="dialog".popovertargetgives the button an expanded state in the accessibility tree only, and no popup type (HTML-AAM). So the Button page's Filters example now setsaria-haspopup="dialog"in all seven frameworks, and the Popover setsaria-expandedon the trigger that opened it (and"false"on every trigger while it's closed), which the Button's Expanded style and Playwright read, asbutton/popup-expandedasks. A new rule,popover/trigger-expanded, covers it. There's notriggerslot, as on the Dialog. The Popover finds its trigger fromToggleEvent.source(Baseline 2026), or else the first[popovertarget]that points at it, and only that trigger carries the anchor's name, so a popover with two triggers opens beside the one people pressed.openis two-way. The trigger opens it with no state at all, soopenfollows every open and close:v-model:openin Vue,bind:openin Svelte,[(open)]in Angular, and the nativetoggleevent in React and Solid, the way they follow a Checkbox'schange. Whichever acts last wins, and settingopenfires noclose. Flagged: this differs from the Dialog's one-wayopen, since the browser, not the page, opens and closes a popover.data-autofocus, or else the first control in the body, then the close button (the Dialog's order), since the platform only honoursautofocus. It goes back to the trigger when the popover closes from inside, which the Popover does itself, since Safari doesn't focus a button on click. After a press outside, it stays where people pressed (HTML Standard).Tabout of the last control leaves the popover open, as the platform does, which WCAG 2.4.11 allows becauseEscapecloses it from anywhere without moving focus. Flagged: closing onTabout is a defensible alternative.Escape. The Popover listens on itself and on its trigger, skips a key something inside already cancelled, then cancels and stops it. Inside a Dialog, which cancelsEscapebefore the browser could close the popover, it also listens on the Dialog's parts, which hear anEscapeafter the control that has focus and before the Dialog, so a control that handlesEscapeitself still goes first (dialog.md's rule). With a popover open inside it,Escapeis the inner one's. It also listens on the Dialog itself, in the capture phase, for a key while no control has focus, like after a click on text in the popover. When the Dialog closes, fromEscapeor its own buttons, the popover closes with it. A Tooltip inside closes first and alone, and a Dialog around it stays open, with focus in the popover or on its trigger. It doesn't copy the Tooltip'sdocumentlistener, which would run before the Tooltip's own. Tests mount the real Dialog and Tooltip for all three cases.placement="bottom", with an arrow.bottomdiffers from the Tooltip'stop: a popover is something people asked for and act in, so it opens where their eyes go next, below the button, as Radix and Base UI do. The arrow is on by default, as on the Tooltip, since it shows at a glance which button a popover belongs to, andarrow={false}turns it off. The placement names, CSS anchor positioning and the scripted fallback are the Tooltip's, in a copy of its own inPopover.vue, since extracting them would change the Tooltip's reference code.--popover--width, never wider than the screen less 32px), with its padding, corners, border, shadow and type, a title at the text's own line height, and anxsclose Button, 24 × 24px, the minimum WCAG 2.5.8 allows. The anatomy draws its panel with the same values.Escapeor press outside without leaving the popover first, so every popover has one, anxsButton (the Dialog's issm) named bycloseLabel, as on the Dialog.startandendopen below or above it, and@position-tryfallbacks shrink it on a side with about half the screen or more, with the body scrolling, so it stays inside 320 × 256 (400% zoom) and never covers its trigger in browsers with CSS anchor positioning. In older ones (Chrome and Edge before 129, Firefox before 147, Safari before 26), the scripted fallback keeps it inside the viewport, but a tall popover can cover its trigger, and the page says so, with those versions.position-try-order: most-heightwas tried and dropped, since Chrome sorts the base position with it and overridesplacement. No bottom sheet, which would move the content away from its trigger and is a Dialog in all but name.popoverTitleandcloseRequest, for the same reasons as the Dialog'sdialogTitleandcloseRequest.The Dialog, Tooltip and Button pages now link to the Popover where they called it upcoming, and in those sentences the Dialog and Tooltip link to each other too. The Roadmap ticks it, and the changelog has an entry.
At the owner's request, the Radio Group page is renamed from
5.radio-group.mdtoradio-group.md, content unchanged, so CI'sTestpasses. The same rename reachedmainfirst, with the Button page's split (#54), so this branch takes it frommainrather than making it again. The Button's Filters example and its "With other components" bullet now live in the Button's DX part,button/3.dx.md.Checklist
feat(docs): add the focus guideline.pnpm check-typespasses.pnpm dev. (Not run. Read on the built site instead, served fromdist/, in Chrome at 320 × 256, 390 and 1280px wide, in light and dark, with the keyboard, underdir="rtl"and in Chrome's accessibility tree.)pnpm build, thenpnpm lighthouse, still scores 100 in every category. (No homepage changes.)