Skip to content

Panel features

Most of these behaviors are shared across every Real A11y surface — the Chrome extension, the @real-a11y-dev/inspector embed, the React component, and the Storybook addon. A few exist only in the Chrome extension, and their sections say so — live region monitoring is one. Where the per-package APIs differ, this page calls out which prop / option to reach for.


A live filter over the tree. Matches on tag name, role, accessible name, attributes, and visible text. Type "email" → every input, label, and heading whose name or content contains "email" stays visible; everything else collapses. Clearing the field restores the full tree.

Works in DOM, A11y, and TAB views.

SurfaceHow it appears
Chrome extensionThe "Search nodes…" input at the top of the panel toolbar
inspector / React <SemanticNavigator />Same input, rendered by @real-a11y-dev/semantic-navigator-ui
Storybook addonSame input in the addon panel header

Role filters

Six chip-style toggles that hide every node except the matching role (and the ancestors needed to reach it). Click the same chip again to clear.

FilterWhat it showsWhat it's for
Headingsh1–h6 and role="heading"Verify the document outline — exactly one h1, no skipped levels
Links<a href> and role="link"Audit link text — every link should have a unique, meaningful name
Buttons<button> and role="button"Catch icon-only buttons missing aria-label
FormsEvery form control with its labelCatch unlabeled fields fast
Landmarksmain, banner, contentinfo, nav, region, etc.Verify structural landmarks — one main, named navs when there are several
Images<img> and role="img" with their alt/aria-labelEmpty alts (decorative) stand out from missing alts (a bug)

Disabled in TAB view — the tab sequence is already a flat projection; filtering it by role would double-filter.

SurfaceHow to enable
Chrome extensionThe chip row above the tree
inspector / ReactBuilt into the panel UI; nothing to wire up
Storybook addonSame chip row in the addon panel

Focus tracking

When focus tracking is on, the panel and the host page stay in sync bidirectionally:

  • Page → panel — when focus moves on the page (Tab, click on a focusable element), the panel draws the highlight overlay on that element, scrolls the tree to its node, and selects the row. This is what the "Focus sync" toggle controls. Implemented by the Chrome extension via a focusin listener in its content script, for its in-page tree and for Chrome's own tree alike; the in-page surfaces don't ship this auto-sync (the host app would have to wire it up itself).
  • Panel → page — selecting a tree node draws the highlight overlay on the corresponding element. The Chrome extension also moves real focus to that element on selection — its DevTools panel lives in a separate window, so it can do that without disrupting panel navigation. For the in-page surfaces (inspector, React <SemanticNavigator />), focus and scroll side effects on the host element are opt-in, to avoid stealing focus from the panel itself. See Panel interaction vs. host side effects.

When to leave it on

The default. It's the single strongest feature for understanding "what a keyboard user is looking at right now." Tab through a form with the panel open and you'll see the experience.

When to turn it off

Three cases:

  1. Focus-heavy pages — forms with 30+ fields, SPAs that grab focus on every interaction, or pages with aggressive focus-trap libraries. The tree would jump around continuously; turning the sync off keeps it stable on the node you're inspecting.
  2. Auditing a specific subtree — when you've scoped into a dialog or region and want to prod the page without losing your place.
  3. Curtain mode (extension only) — with the page blacked out, the overlay is suppressed anyway; turning the sync off avoids unnecessary message traffic.

How to verify it's working

Focus the page itself first (click anywhere on the page, not the panel), then press Tab. You should see both a blue rectangle on the focused page element and the corresponding tree node highlight in the panel. If only one happens — or neither — see the troubleshooting page.

SurfaceAPI
Chrome extension"Focus sync" / "Focus OFF" toggle in the toolbar, for either tree — controls page → panel sync; panel → page focus is always on (separate window, no conflict)
inspectorcreateInspector({ root, container, highlightOnHover: true, scrollHostOnSelect: true, focusHostOnActivate: true }) — opt in to overlay / scroll / focus on the host page; all three default to off, see the rationale
React <SemanticNavigator /><SemanticNavigator highlightOnHover scrollHostOnSelect focusHostOnActivate /> — same opt-ins as the inspector; see the package reference
Storybook addonSelection draws an overlay on the story preview by default; the addon hardcodes safe values since the manager iframe is isolated from the preview

Scoping

Narrow the audit root to a subtree. Useful when you want to focus on one form, one dialog, or one widget without the rest of the page in the way.

In the Chrome extension's panel, double-click a container node — a form, a landmark, a list, anything with children and no action of its own — to scope into it, or select any row with children and press Ctrl/Cmd+Enter. Double-clicking an interactive node still activates it. The tree re-renders showing only that subtree, and a breadcrumb at the top shows the path: click any breadcrumb segment to scope back up, or Exit scope (✕), or press Esc in the tree or in a list, to return to the full tree. Both the DOM and the native tree scope the same way, with one difference in what counts as interactive: the DOM tree activates (focuses) any focusable container, such as a tabindex="0" region, while the native tree scopes into it, since selecting a native row already moves page focus there.

Scoping affects:

  • What's visible in the tree (only the scoped subtree's descendants)
  • What search matches (only rows inside the scope show, and the match count reads "in this scope" and counts only those)
  • What the role-filter lists show (Headings, Links, … list only matches inside the scope)
  • The tab sequence (TAB view only lists focusable elements inside the scope)
  • What Copy exports (the scoped subtree, with a Scope line naming it)

Picking an element outside the scope, or focus moving to one while focus tracking is on, leaves the scope so the row you land on is actually shown. A list can't take you outside the scope, since it only lists what's inside it. In a list, Esc leaves the scope as it does in the tree, and Ctrl/Cmd+Enter does nothing rather than activating the item.

SurfaceAPI
Chrome extensionDouble-click a container node, or Ctrl/Cmd+Enter on any row with children; breadcrumb, Exit scope (✕) or Esc to exit
inspectorinspector.setRoot(element) to scope; inspector.setRoot(originalRoot) to exit
React <SemanticNavigator />Update the root prop's ref to point at the scoped element
@real-a11y-dev/testingattach(page, { rootSelector: "form" }) for Playwright; treeSnapshot(form, …) for jsdom — see the testing reference

Dialog auto-scope

When the page opens a <dialog> with showModal(), the tree re-roots to that dialog: the dialog becomes the root row, and the page behind it drops out. This is what a screen reader user effectively gets. The browser makes the rest of the page inert, and the dialog is the only thing they can navigate.

It does this wherever the dialog is on the page, including outside the element an in-page panel is scoped to, and it re-roots back when the dialog closes or is removed. The exception is shadow DOM: a dialog inside a shadow root doesn't re-root the tree, and no dialog re-roots a panel whose root is inside a shadow root.

Only a modal <dialog> does this. A <dialog> opened with show(), or an element with role="dialog" or aria-modal="true", does not re-root the tree, and the page stays in the tree around it. If one of these is outside an in-page panel's root, it joins the tree only when it has a role of dialog or alertdialog, which widens the tree to the whole page. A plain <dialog> opened with show() there stays out of the tree. Dialog libraries such as Radix, MUI and Headless UI hide the rest of the page with aria-hidden or inert, so that content drops out of the tree too. The tree then shows the dialog alone, but its root is still the page, not the dialog, so the extension shows no Dialog: … bar for it.

The re-rooting is shared: every panel builds its tree with the same extractor. What the panel shows around it differs.

SurfaceWhen a modal <dialog> opens
Chrome extensionA Dialog: … bar appears above the tree with the dialog's accessible name. A dialog with no name reads Dialog: Modal. The bar's Press ESC button sends Escape to the page and closes the dialog, unless a page handler calls preventDefault()
inspector / React <SemanticNavigator />No indicator. The dialog is the tree's root row. The panel is part of the page, so it goes inert along with everything else behind the dialog: you can't click, focus or type in it until the dialog closes. In React, useActiveModal() returns the dialog's node if you want to render your own indicator
Storybook addonNo indicator. The dialog is the tree's root row. The panel lives in Storybook's manager, outside the story's iframe, so it stays usable while the dialog is open

The dialog's name is the first thing to check: it is the root row's name, and in the extension it is also in the bar. The panel doesn't test that focus stays in the dialog or that Escape closes it. Those are the dialog's own behavior, so check them by hand: Tab through the dialog, then press Escape. In the extension, the keyboard bar's Tab and Esc buttons do both from the side panel.


Live region monitoring

Chrome extension only. The inspector, React <SemanticNavigator /> and Storybook addon panels have no live log; nothing in them watches the page's live regions.

While the side panel is open, when a live region on the page changes or is shown, its new text appears in the live log at the bottom of the side panel. A live region is a role="status", role="log" or role="alert", or anything with aria-live="polite" or "assertive"; aria-live="off" silences even an alert. Each entry shows the region's role (status for an aria-live element with no role), then its text. Assertive entries are marked red, polite ones blue. The log holds the last five entries, and each clears after 8 seconds.

Use this to verify:

  • Toast notifications announce on appearance
  • Form validation errors announce when triggered
  • Loading indicators announce both their start ("Loading…") and completion ("Loaded.")
  • Search-result counts announce as you type

An entry's text is what a screen reader could announce: nothing hidden or aria-hidden, and never a form field's markup text. Text a region already held when the panel opened is not logged until it changes. Even so, the log approximates a screen reader rather than recording one. Each entry carries the region's whole text after the change, not only the part that changed. Changes within 200 ms collapse into one entry. Only the top frame is watched, so a live region inside an iframe never appears.

SurfaceWhere the log appears
Chrome extensionBottom of the side panel
inspector / ReactNot available
Storybook addonNot available

Keyboard navigation

The panel itself is fully keyboard-operable.

KeyAction
↑ / ↓Move between tree rows
→Expand the current row
←Collapse the current row, or move to its parent
EnterActivate the current node — clicks links, submits buttons, opens menus. On a slider/spinbutton, steps the value up
Shift+EnterOn a slider/spinbutton, step the value down (the ▼/▲ buttons are mouse-only)
+ / =On a slider/spinbutton, step the value up
- / _On a slider/spinbutton, step the value down
SpaceSame as Enter for most elements; toggles checkboxes/radios
/Focus the search input
Home / EndJump to the first / last visible row
Printable charactersType-ahead — jump to a row whose accessible name starts with the typed characters (multi-character within ~500ms keeps a still-matching selection; repeating the same letter cycles matches). / is reserved for search.
Alt+J / Alt+Shift+JFollow the current row's aria-controls jump chips: Alt+J goes to the first row it controls, and pressed again moves on to the next; Alt+Shift+J goes back to the row you jumped from, or to the row that controls this one. The chips sit outside the Tab order, so this is their keyboard path. A search hiding the target is cleared
Ctrl/Cmd+EnterChrome extension: scope the tree to the current row (any row with children)
EscChrome extension: leave the current scope, from the tree or from a role-filter or Tab-sequence list. In the native tree, while a pick is armed, Esc cancels the pick instead

Implemented in @real-a11y-dev/semantic-navigator-ui via useTreeKeyboard (and the same type-ahead + stepper-key helpers on filtered / tab-sequence listboxes) — every package that mounts the tree gets the same keymap. The two scope keys belong to the Chrome extension's panel, the one surface with a scope breadcrumb.


Panel interaction vs. host side effects

A distinction worth pinning down before wiring up an in-page panel for the first time, because the framing "interaction is opt-in" gets it backwards:

  • Panel-internal interaction is always on, in every surface. Selecting a row, clicking a cross-link chip and jumping to its target, expanding / collapsing, arrow-key tree movement — all of this mutates panel state only. Nothing you do inside the panel reaches the real DOM until you ask it to. This is identical across the Chrome extension, the inspector embed, the React component, and the Storybook addon.
  • Host-page side effects are what's gated. Calling .focus() or .scrollIntoView() on the real element in response to a panel action — that's opt-in for the in-page surfaces and on by default for the isolated ones.
SurfaceWhere the panel rendershighlightOnHover / scrollHostOnSelect / focusHostOnActivate
Chrome extensionDevTools panel (separate window)On by default — no focus/scroll conflict possible
Storybook addonManager iframe (separate document from story preview)Hardcoded to safe values — addon doesn't expose the props
@real-a11y-dev/inspectorSame document as the audited appOff by default — opt in via config
React <SemanticNavigator />Same document as the audited appOff by default — opt in via props

Why the in-page surfaces default to off

If activating a tree row stole focus to the host element, every Enter / Space on a row would yank focus out of the panel and break tree-pattern keyboard navigation. Same for scroll — every arrow-key step would scroll the page out from under you. The extension and Storybook addon don't have that problem because the panel and the inspected DOM live in different windows / iframes; .focus() on a host element in one document can't pull focus out of a panel in another.

The broader principle, recorded in the changelog when these defaults were established: opt-in host-app side effects so audits and test harnesses don't disturb the app under test.

Opting in for the in-page surfaces

ts
// Vanilla
createInspector({
  root,
  container,
  highlightOnHover: true,
  scrollHostOnSelect: true,
  focusHostOnActivate: true,
});
tsx
// React
<SemanticNavigator
  root={rootRef}
  highlightOnHover
  scrollHostOnSelect
  focusHostOnActivate
/>

highlightOnHover is the safest of the three to enable — the overlay just paints over the host page without touching its layout or focus. scrollHostOnSelect and focusHostOnActivate are the ones that affect the page underneath; turn them on when the panel is set up as an active inspection tool, leave them off when the panel is mounted in production for end users.


See also

Released under the MIT License. · Privacy · Accessibility