What goes wrong
- Hover-only UI: keyboard and touch users can’t open it.
- No relationship: trigger doesn’t expose state (
aria-expanded) or control (aria-controls). - No name/description: assistive tech announces a generic container with no context.
- Can’t close: no close button, no
Esc, and click-outside doesn’t dismiss. - Focus loss: popover opens visually but focus stays elsewhere, so keyboard users don’t know it appeared.
Small, reliable pattern
- Trigger is a button (not a
div) and exposes state:aria-expanded. - Connect trigger → popover with
aria-controls. - Name the popover with
aria-labelledby(and optionallyaria-describedby). - Move focus in on open (often to the popover container or first actionable control).
- Close paths: close button,
Esc, and (often) click outside. - Restore focus to the trigger when closing.
If your popover behaves like a modal, consider using a real modal dialog instead. If it’s just helper text, consider inline helper text (no popover).
Checklist
- Trigger can be reached by
Tab. - Trigger indicates state (
aria-expandedtoggles). - Popover has a programmatic name (
aria-labelledby). - Popover can be closed with
Escand a visible close button. - Focus doesn’t get lost; it goes into the popover on open and returns to trigger on close.
- Screen reader users are not forced through background content unexpectedly.
Minimal snippet (concept)
<button type="button"
aria-haspopup="dialog"
aria-expanded="false"
aria-controls="pop"
>Why?</button>
<div id="pop"
role="dialog"
aria-modal="false"
aria-labelledby="popTitle"
hidden
tabindex="-1"
>
<h3 id="popTitle">Why we ask</h3>
<button type="button">Close</button>
</div>
// JS: toggle [hidden], update aria-expanded,
// focus() popover on open, close on Esc/outside click,
// restore focus to trigger.
Note: role="dialog" is reasonable for a popover that contains interactive controls. Don’t use menu roles unless it’s truly a menu.