Click to component
August 25, 2026 · View on GitHub
Alt+Click any rendered element to jump straight to the JSX line that created it, in your editor. On macOS/XQuartz that's Option+Click — Option is what XQuartz maps to X11's Mod1 by default; the installed-feature log line says whichever one applies to your platform.
Setup
REACT_X11_EDITOR=code npm run examples:tasks # naming an editor enables it
# or, to use the default (cursor):
REACT_X11_CLICK_TO_COMPONENT=1 npm run examples:tasks
Alt/Option+Click a task row, a button, anything — its owning component's source opens at the exact line, the clicked element flashes blue for a moment, and the console prints what was resolved:
[click-to-component] TaskRow → examples/tasks.jsx:42:7
Alt+Shift+Click also logs the full ownership chain (who rendered who, up to the root) to the console — handy when the immediate owner is a shared wrapper rather than the component you actually meant.
Elements you didn't write
An element rendered by an installed component — a design system's
<Toolbar>, a chart library's internals — has its JSX call site inside that
package, not in your code. Clicking one resolves to the nearest owner up the
chain that is your source: the <Toolbar ... /> line in your file that put
it on screen. The console line says how far it had to climb, so a location
that doesn't match what you clicked is never a surprise:
[click-to-component] App → src/ui/App.tsx:432:7 (1 owner up from the clicked <box>)
When no owner in the chain is application source (a screen rendered entirely
by a package), the package's own file is opened instead and the line is
marked — inside an installed package. Only a tree with no React debug info
at all resolves to nothing.
With React DevTools attached as well (REACT_X11_DEVTOOLS=1, see
devtools.md), the same click also selects that element in the
DevTools tree — one click to the source and to its props. Nothing changes
when DevTools is absent, which is the usual case.
REACT_X11_EDITOR picks the editor (cursor by default): code,
code-insiders, windsurf, vim, nvim, or any other value, treated as a
custom URI scheme.
How it works
src/ClickToComponent.js, installed from Reconciler.js when either
REACT_X11_CLICK_TO_COMPONENT or REACT_X11_EDITOR is set. No
babel/jsx-source plugin, no build-time instrumentation:
- every host node already carries a reference to the fiber that created it
(
node._reactFiber, set increateInstance). It is this feature's own back-reference, not a DevTools one:findFiberByHostInstanceis gone from react-reconciler 0.33, and click-to-component works with DevTools absent; - React 19 captures a real
Errorat every JSX call site in development mode (fiber._debugStack) and tracks who rendered what (fiber._debugOwner) — no__source/__selfbabel transform needed; - Node's
--enable-source-maps(already on for both the plaintsxexamples andexamples:tasks:hot's loader) has rewritten that Error's stack to original source before we ever see it, so resolving a location is just parsing stack-trace text and skipping the frames React and this renderer add on top of the call site (react,react-reconciler,scheduler,react-x11's ownsrc/) — no source-map library required (this is the one place the design diverges from a browser DOM version of the same idea: a browser doesn't remapError.stackfor you, so a tool like show-component has to resolve source maps itself at runtime).
Frames from other packages are deliberately kept rather than skipped: a
call site inside an installed component is a real location, it just isn't
one in your source, and telling those apart is what lets the resolver climb
the _debugOwner chain to the line that is. Skipping every node_modules
frame instead walks past the library component entirely and lands on
whichever frame of yours happens to be deeper in the render stack — usually
the render() call at startup, which looks like an answer and isn't.
Alt+Click is recognized in EventManager._onMouseDown
(src/events.js) — native.buttons & MOD.Alt is X11's Mod1Mask bit, the
same buttons bitmask every synthetic event's altKey reads — ahead of the
normal press handling, so it never also starts a drag or moves focus.
GUI editors (cursor/code/code-insiders/windsurf, or any unrecognized
REACT_X11_EDITOR value treated as a raw scheme) are opened by navigating
their registered URI scheme — open cursor://file/abs/path:line:col on
macOS, xdg-open elsewhere — the same idea show-component uses in a
browser via window.location.href, just handed to the OS instead. This is
noticeably faster than spawning the editor's CLI shim directly: a CLI like
cursor -g ... is a wrapper script that re-detects and re-execs the real
app on every call, while the URI scheme is delivered straight to the
already-running instance by Launch Services / xdg-open. vim and nvim have
no URI scheme to navigate, so those are still spawned directly with a
+call cursor(line, column) argument.
Caveats
- Needs React running in development mode (fiber debug fields are stripped in production builds).
- The editor is launched detached, so a failure is only visible as a
warning: an editor whose URI scheme nothing claims (
REACT_X11_EDITORnaming an editor that isn't installed) printsclick-to-component — \open cursor://...` exited 1` next to the resolved location rather than silently doing nothing. - A node's
_reactFiberis only refreshed on mount, not on every update — usually harmless (the JSX call site of a given element rarely moves between renders of the same tree shape), but a node that was conditionally re-typed can point at a stale-but-structurally-similar fiber.