The gem leans on the native <dialog> element and the Popover API so
most assistive-tech behavior comes from the browser. This page lists
what's automatic, what you're responsible for, and what's known not to
work.
- Focus management. Modal and drawer dialogs open via
dialog.showModal(), which moves focus into the dialog and traps it inside.dialog.close()restores focus to the element that was focused before the dialog opened. Popovers and hints use the Popover API (showPopover()/hidePopover()) which provides the same restoration. Frame re-renders inside an open overlay preserve the dialog node via Turbo morph, so focus survives a server-side validation round-trip. - ESC dismissal. Native for modal/drawer (the
cancelevent); synthesised in the Stimulus controller for non-modal drawers and popovers, where the platform'scancelevent doesn't fire. Stacked overlays only dismiss the topmost layer per press. - Labelled dialogs.
aria-labelledbyis wired to the<h2>rendered fromoverlay_title. Provide anoverlay_titleon every overlay view and screen readers will announce the dialog by name. - Triggers signal what they open.
modal_link_to,drawer_link_to, andpopover_link_toemitaria-haspopup="dialog".hint_link_to(and overlay links composed withhint: true) emitaria-haspopup="tooltip". Override witharia: { haspopup: ... }or"aria-haspopup" => ...when a different semantic fits. - Close buttons are named. The default chrome's
×button carriesaria-label="Close"; the glyph itself is wrapped inaria-hidden="true"so it never reaches AT as "times". - Keyboard focus is visible. The plain theme adds a
:focus-visibleoutline (currentColor) on close buttons; Tailwind and Bootstrap themes inherit framework focus styles. - Loading announcements. The placeholder dialog opens with
role="status" aria-live="polite"and a visually-hidden "Loading…" text node, so screen readers announce when an overlay starts fetching. - Reduced motion. All animations (open/close, slide-in/out,
backdrop fade, hint enter/leave, loading spinner) respect
prefers-reduced-motion: reduce. - Hint affordances. Hints render as
role="tooltip", link to their trigger viaaria-describedbywhile visible, skip touch devices, and dismiss on focusout / ESC / Turbo navigation. See hints.md.
- An accessible title. Set
overlay_titlein every overlay view. Without it thearia-labelledbyreference points to a missing element and the dialog has no accessible name. - Accessible trigger text.
modal_link_to "Edit", …reads "Edit, has popup, dialog" — verify the visible text on every trigger makes sense out of context. Avoid bare icon triggers withoutaria-label. - Sensible focus order inside the dialog. The native focus trap keeps focus inside the dialog; the order it cycles through is your template's tab order. Keep destructive actions (Delete) away from the dialog's first focusable element.
- Non-modal drawer inside an open modal is unsupported. The HTML
inertness algorithm makes everything outside the topmost modal
inert, so a
backdrop: falsedrawer opened from inside a modal renders behind the modal and ignores input. See popovers.md. Popovers opened from inside a modal are auto-promoted toshowModal()to dodge the same trap (the::backdropis rendered transparent so the visual stays "popover-like"). - Animated close mid-stack. A middle-of-stack
<dialog>may skip its close animation in Chromium/Firefox when a higher dialog is also closing. This is a paint quirk, not an a11y blocker — focus and semantics are unaffected. aria-modal="true"is not set on the native<dialog>. Browser modal semantics come fromshowModal()itself; the WAI-ARIA APG advises against duplicating the attribute. Some older AT may behave differently than modern AT; that's a platform issue, not something the gem can paper over.