@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.
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.