Prompt and scope
This is a modern Web Platform and accessibility interaction problem. The goal is a menu, filter panel, or help surface that leaves the page usable while supporting light-dismiss. The HTML Popover API uses popover, popovertarget, and the top layer to provide a browser-managed display lifecycle. Do not migrate every overlay to the native API; positioning itself is not the primary exercise.
What the interviewer is testing
- Distinguishing
popover="auto",popover="manual", and modal dialog semantics. - Keeping the trigger, surface, and close actions semantically understandable.
- Handling Escape, outside clicks, multiple surfaces, focus return, and lifecycle events.
- Explaining progressive enhancement, browser support, and a non-breaking fallback.
Clarifications to ask first
Confirm whether the background remains interactive. If it must be blocked, use <dialog> with showModal() rather than popover. Clarify whether only one surface may be open, whether several manual surfaces may coexist, whether the trigger toggles, whether a successful form submission closes it, and the minimum browser range. A filter surface is usually non-modal, but keyboard users still need an explicit focus and close path.
A 30-second answer
Start with semantic HTML: put popovertarget on the trigger and popover="auto" on the surface; use manual only for custom close orchestration. The browser supplies top-layer behavior, Escape, and outside light-dismiss. I add focus entry, focus return, status text, and beforetoggle/toggle lifecycle handling. Unsupported browsers keep the same DOM and names with a small polyfill instead of a global click handler that can close unrelated surfaces.
Step-by-step solution
1. Choose popover or modal dialog
Popover fits menus, filters, non-blocking help, and temporary actions; the page stays non-modal. <dialog> with showModal() fits confirmations, payment, or warnings that must be handled first because it supplies modal semantics and blocks the background. Both can use the top layer, but their accessible names, focus strategy, and close reasons differ; replacing dialog with a popover attribute is not equivalent.
2. Establish the trigger and surface relationship
The smallest semantic structure lets the browser maintain the target relationship:
<button type="button" popovertarget="filters" aria-controls="filters">
Filters
</button>
<div id="filters" popover="auto">
<form method="get">
<label>Status <select name="status"><option>All</option></select></label>
<button type="submit">Apply</button>
</form>
</div>popovertarget makes the button a declarative trigger, and the browser manages the relationship during open and close. aria-controls can help assistive technology understand the relation, but it does not replace a visible name or correct focus behavior. Complex components should retain a trigger reference in script and return focus to it only if it still exists and is focusable.
3. Understand auto, manual, and light-dismiss
An auto popover supports browser-managed light-dismiss: Escape or an outside click can close it, and it participates in the automatic closing chain. A manual popover is not closed by those mechanisms; call showPopover(), hidePopover(), or togglePopover() explicitly. Use it when several surfaces may coexist or the lifecycle is custom. Do not add document-level click closing on top of auto; nested surfaces and trigger clicks can race.
4. Handle the top layer, positioning, and backdrop
Once open, a popover enters the top layer and is no longer constrained by an ordinary ancestor’s overflow or z-index stacking context. Use CSS Anchor Positioning or regular layout for placement, with a static fallback when placement fails. A non-modal popover should not automatically receive a full-screen backdrop; if the product needs one, re-check whether the requirement is actually modal dialog behavior. The top layer changes painting order but does not solve narrow viewports, scroll containers, or collision placement.
5. Define focus and keyboard behavior
Move focus into the first actionable control, or keep the browser default only after testing it. On Escape, return focus to the trigger; if the trigger was removed, fall back to the nearest visible context. Tab order should naturally traverse the surface; do not hide all controls with tabindex="-1". A menu-like popover needs menu-item arrow-key and selection semantics, while a filter form should keep ordinary form keyboard behavior.
6. Observe lifecycle instead of guessing global state
Use beforetoggle to validate a transition, record a close reason, or synchronize application state; use toggle after the state actually changes to update labels and focus. Register handlers per element and clean them up on component destruction. For mutually exclusive auto surfaces, rely on the browser’s close stack rather than maintaining a stale global open ID.
7. Coordinate form submission and application state
After a successful filter submission, call hidePopover() explicitly if desired, but do not treat closing as proof of success. On a failed request, keep the surface open and associate the error with the form. Open state is temporary UI state; filter values are business state. Keep them separate, and recompute them on navigation or history changes instead of treating :popover-open as the only source of truth.
8. Use progressive enhancement and test the interaction
Feature-detect the actual API, such as HTMLElement.prototype.showPopover. Use native behavior when available; otherwise preserve the same trigger, content, and accessible names with a small polyfill for display, Escape, outside clicks, and focus return. Test keyboard, touch, zoom, scrolling, nested surfaces, removed triggers, failed forms, and reduced motion. Verify names, state, and errors with a screen reader rather than checking pixels alone.
Model answer
I would define this as a non-modal filter popover, not a dialog. The button uses popovertarget and the panel uses popover="auto"; the browser manages the top layer, Escape, and outside light-dismiss, while script handles form state, focus return, and lifecycle events. On open, focus enters the first control; on close, it returns to the trigger. A failed submission keeps the surface open and exposes the error; success can close it. Use manual only when several custom surfaces must coexist, and do not duplicate document click handling for auto. Provide a static placement fallback and a semantic polyfill when feature detection fails. Test keyboard, touch, scroll, and assistive technology paths.
Common mistakes
- Using popover for a confirmation that must block the background, losing modal semantics.
- Adding document click closing on
auto, which can close nested surfaces or submit controls. - Toggling
displaywithout maintaining the trigger relation, focus return, or error state. - Simulating a dialog with a full-screen backdrop while keeping non-modal behavior.
- Treating the top layer as automatic positioning and ignoring scrolling, narrow viewports, and collision fallback.
- Closing a failed form unconditionally so users cannot see or fix the error.
- Testing only mouse clicks and missing Escape, Tab, touch, and screen-reader behavior.
Follow-up questions
When should you use <dialog> instead?
Use <dialog> with showModal() when the background must be inert, the user must handle a decision first, or modal focus behavior is required. Popover light-dismiss and non-modal semantics cannot replace background blocking and explicit cancel/confirm actions.
How do you make several filter surfaces mutually exclusive?
Prefer auto for mutually exclusive surfaces so the browser manages the close chain. If several manual surfaces may coexist, keep an explicit instance set, close only the targeted instance, and store each instance’s trigger and focus return independently.
How do you support browsers without Popover API?
Feature-detect the API and preserve the same DOM and semantic attributes, with a polyfill for display, close behavior, Escape, outside clicks, and focus return. Do not degrade to an unnamed, keyboard-inaccessible absolutely positioned div; document the compatibility matrix and test it before release.
How do you test light-dismiss boundaries?
Test clicks on the trigger, inside the surface, outside siblings, scroll containers, and another popover. Internal clicks must stay open; outside clicks should close only eligible auto surfaces, and repeated trigger clicks must not create duplicate state. Add Escape and touch tests.
Can animation harm accessibility?
Use prefers-reduced-motion to shorten or remove transitions while keeping focus and readable state consistent. Do not hide focusable controls with opacity alone; prevent duplicate submission during close and expose a stable state change to assistive technology.
What if placement fails?
With anchor positioning, provide a normal-flow or viewport-safe fallback and respond to size and scroll changes. A failure must not clip the content or push it off-screen; the core form remains usable without the positioning enhancement.