@real-a11y-dev/react
TL;DR —
<SemanticNavigator />component plususeSemanticTree()/useActiveModal()hooks. Built onuseSyncExternalStorefor concurrent-mode safety, SSR-safe in floating mode. Reach for this when your app is React — for anything else use@real-a11y-dev/inspector.
Native React integration — hooks and a component. Built on useSyncExternalStore for React 18 concurrent-mode safety.
Install
npm install -D @real-a11y-dev/reactPeer dependencies: react >= 18, react-dom >= 18
Install as a dev dependency
@real-a11y-dev/react bundles a tree extractor and a Preact-based renderer (~40 KB gzipped). It's a developer audit tool, not runtime infrastructure — keep it in devDependencies and gate <SemanticNavigator /> on a build flag so it never ships to end users.
See Keep it out of production for the common Vite / Next.js / vanilla gating patterns.
<SemanticNavigator />
Drop-in tree panel component. Renders into a Shadow DOM by default.
import { useRef } from "react";
import { SemanticNavigator } from "@real-a11y-dev/react";
function App() {
const rootRef = useRef<HTMLDivElement>(null);
return (
<div ref={rootRef}>
<YourApp />
<SemanticNavigator
root={rootRef}
mode="a11y"
/>
</div>
);
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
root | RefObject<Element | null> | — | Required. Ref to the DOM element to observe. |
mode | "a11y" | "dom" | "a11y" | Tree extraction mode. Can be changed at runtime. |
mount | "shadow" | "light" | "shadow" | Shadow DOM isolation mode. |
highlightOnHover | boolean | false | Highlight host element on tree node hover. |
scrollHostOnSelect | boolean | false | Scroll host element into view on selection. |
focusHostOnActivate | boolean | false | Focus host element on action activation. |
styleNonce | string | — | CSP nonce for injected styles. |
className | string | — | Class name applied to the host <div>. |
style | CSSProperties | — | Inline styles for the host <div>. |
The component creates its own internal <div> host and passes it to createSemanticNavigator. Changing root or mount remounts the navigator; changing mode uses the imperative setViewMode() API without remounting.
Why highlightOnHover / scrollHostOnSelect / focusHostOnActivate default to false
<SemanticNavigator /> renders into the same document as your app, so activating a tree row could steal focus from the panel or scroll the page underneath you. The panel itself stays fully interactive either way — row selection, cross-link chip navigation, keyboard movement — what's gated is the side effect on the real DOM element. See Panel interaction vs. host side effects for the full rationale and when to flip them on.
useSemanticTree(rootRef, options?)
Subscribes to the semantic tree for a given DOM element. Re-renders whenever the DOM mutates and the debounce settles.
import { useRef } from "react";
import { useSemanticTree } from "@real-a11y-dev/react";
import { findByRole } from "@real-a11y-dev/core";
function FocusAnnouncer({ rootRef }) {
const tree = useSemanticTree(rootRef, { mode: "a11y" });
const dialog = tree ? findByRole(tree, "dialog") : null;
return (
<div aria-live="polite">
{dialog ? `Dialog open: ${dialog.a11y.name}` : null}
</div>
);
}Returns: SemanticTree | null — null before the first extraction.
Options:
| Option | Type | Default | Description |
|---|---|---|---|
mode | "a11y" | "dom" | "a11y" | Tree extraction mode. |
debounceMs | number | 300 | Debounce delay for DOM mutation callbacks. |
Concurrent-mode safety
useSemanticTree uses useSyncExternalStore internally. The subscription and snapshot functions satisfy React 18's requirements:
- Snapshot is stable when the tree hasn't changed.
- Subscription calls the listener synchronously on mutation flush.
- No tearing between render and paint.
useActiveModal(rootRef)
Convenience hook — returns the active modal node (dialog or alertdialog) or null.
import { useActiveModal } from "@real-a11y-dev/react";
function ModalGuard({ rootRef }) {
const modal = useActiveModal(rootRef);
if (!modal) return null;
return (
<div role="status">
Modal open: {modal.a11y.name || "(no name — add aria-labelledby)"}
</div>
);
}Returns: SemanticNode | null
Announcing modal opens in an aria-live region
A static role="status" only announces the current state. To tell screen-reader users that a dialog just opened, wrap the output in a polite aria-live region so each change is announced:
import { useActiveModal } from "@real-a11y-dev/react";
export function ModalAnnouncer({ rootRef }) {
const modal = useActiveModal(rootRef);
return (
<div aria-live="polite" aria-atomic="true" className="sr-only">
{modal ? `Dialog open: ${modal.a11y.name || "unnamed dialog"}` : ""}
</div>
);
}Drop this near the top of your layout alongside the <SemanticNavigator /> host. Users of assistive tech get a courteous notification every time a dialog opens — useful when your app's own announcement logic is still under construction.
Patterns
Audit overlay in development
Gate the import itself so the inspector code is tree-shaken from production bundles (a top-level SemanticNavigator import prevents that — the reference survives even inside an if (DEV)).
// DevAuditOverlay.tsx
import { lazy, Suspense, useRef } from "react";
const SemanticNavigator = lazy(() =>
import("@real-a11y-dev/react").then((m) => ({ default: m.SemanticNavigator }))
);
export function DevAuditOverlay({ children }) {
// Vite: `import.meta.env.DEV`. Webpack/Next: `process.env.NODE_ENV !== "production"`.
if (!import.meta.env.DEV) return <>{children}</>;
const rootRef = useRef<HTMLDivElement>(null);
return (
<div ref={rootRef} style={{ display: "grid", gridTemplateColumns: "1fr 380px" }}>
<div>{children}</div>
<Suspense fallback={null}>
<SemanticNavigator
root={rootRef}
mode="a11y"
highlightOnHover
style={{ height: "100vh", overflow: "hidden", borderLeft: "1px solid #eee" }}
/>
</Suspense>
</div>
);
}Production builds: the DEV branch is dead-code-eliminated and @real-a11y-dev/react never enters the final bundle. Dev builds: the inspector loads on demand as its own chunk.
Reacting to tree changes
import { useRef, useEffect } from "react";
import { useSemanticTree } from "@real-a11y-dev/react";
import { findAllByRole } from "@real-a11y-dev/core";
function A11yBadge({ rootRef }) {
const tree = useSemanticTree(rootRef);
const issues = tree
? findAllByRole(tree, "button").filter(
(btn) => !btn.a11y.name
)
: [];
if (issues.length === 0) return null;
return (
<div style={{ color: "red" }}>
⚠ {issues.length} unlabeled button(s)
</div>
);
}TypeScript
All hooks and components are fully typed. Import types from @real-a11y-dev/core for tree node types:
import type { SemanticTree, SemanticNode } from "@real-a11y-dev/core";See it running
- Vite + React 18 —
examples/react-app/: split-panel layout with a mode toggle,useSemanticTreedriving a live "issues" badge, anduseActiveModalwith anaria-liveannouncer. - Next.js (App Router + React 19) — the Next.js recipe covers the client-component and SSR-gating patterns specific to Next.
Panel features
<SemanticNavigator /> exposes the same in-panel behaviors as the Chrome extension and Storybook addon — search, role filters, focus tracking, scoping, live region monitoring, keyboard navigation. The props on this component (highlightOnHover, scrollHostOnSelect, focusHostOnActivate, mode, mount) configure them.