Stranded at Content Width
Written later and filed to this date, as part of filling in every day of the year.
Pressing Escape on my dashboard’s snooze menu closed the menu and wiped the feed’s search box on the same keypress, because one key handler served both and nothing told it to stop after the first. Clicking anywhere else closed the menu through a document-wide listener registered in the capture phase, the kind that outlives whatever it was written for. And the menu sat at a z-index it could lose to any panel added later with a bigger number. Three small bugs, one cause: the menu was doing by hand what the browser had been offering for free.
The conversion is a pair of attributes. popover="auto" on the menu, popovertarget on the button that opens it, and the browser owns light dismiss, Escape, and the top layer, which is the thing that ends z-index arithmetic for good. Escape closes just the menu. One detail rode along: handing the anchor button its popoverTargetElement is what stops the opening click from counting as a click outside and dismissing the menu it just opened.
Then the phone. On a narrow screen the menu is a bottom sheet pinned by left and right offsets, and after the conversion it was stranded at the width of its own text. The browser’s default stylesheet for a popover sets width: fit-content, and that default outranks a left/right pair. width: auto is one line, and it is load-bearing.
Verified in a real browser, because no version of jsdom implements any of this: open, Escape with the search intact, click-away, a snooze round-trip; the sheet measuring 416 by 162 pixels above a dimmed backdrop, rows 48 pixels tall. That gap in the suite is accepted on purpose. The behaviour left uncovered is the browser’s, not mine, and a shim that fakes support ends up testing the shim.
The rule that came out of it covers the family. Menus, tooltips and transient panels are popovers, since a hover-only tooltip is not a tooltip on a phone. Confirms and modals are <dialog>, for the focus trap and the inert background the hand-rolled ones never had. Accordions are <details name="...">, one attribute in place of the exclusive-open script. Below-the-fold images get loading="lazy". The public site stays on the widely supported subset; everything else here is opened by my own current browsers and can take the newer primitives at once. And nothing that works gets converted for tidiness: all the overlay code on that dashboard is about one percent of its payload, and the payoff is per new feature, in a class of bug that stops being possible.
Native overlays first: before writing JavaScript for anything that opens, closes, floats or hides, reach for the HTML element that already does it, and keep one eye on the browser’s defaults, which outrank yours.