The Setup

I was building an ecommerce business's interactive org chart — Next.js, React Flow (@xyflow/react, the node-graph UI library).

Each person card is a custom React Flow node with a button inside that opens a detail drawer.

Simple enough.

The Wall I Almost Didn't See

I drove it with CDP (Chrome DevTools Protocol, how I automate Arc to test UI), dispatched a .click() on the button, the drawer opened.

Green. Ship it.

Except… real taps did nothing.

Not "did nothing and logged an error." Just silently nothing. The button was visually there. Styled correctly. Real finger, real mouse — nothing.

What I Tried (That Didn't Work)

I went down the usual suspects: z-index, event propagation, the drawer overlay eating events…

I was chasing the WRONG layer.

The actual problem was one level up, and I almost missed it completely because my test passed.

The Fix

Here's what was actually happening.

When a React Flow node is non-selectable, non-draggable, AND non-connectable, React Flow sets pointer-events:none on .react-flow__node.

The whole node. Gone from the pointer event chain.

So every real pointer event fell straight through to the canvas pan layer beneath it. The button's click handler never fired from a genuine tap.

But element.click() — the synthetic version I was testing with — bypasses pointer-events entirely. It fires the handler directly. So my CDP test returned green while real users were tapping into the void.

Two-line fix:

  • elementsSelectable: true on the <ReactFlow> component — keeps pointer-events alive on nodes.
  • nopan class on the inner button — React Flow treats nopan elements as non-pan zones, so a click-drag on the button doesn't accidentally pan the canvas.

How to Verify (Don't Trust the Synthetic Click)

Two checks that actually tell you the truth:

  • getComputedStyle(node).pointerEvents === "all" on the .react-flow__node element (not "none")
  • document.elementFromPoint(centerX, centerY) at the button's center lands on the button, not a canvas overlay underneath it

If elementFromPoint returns something in the canvas layer, a real tap will never reach your button — no matter what your click test says.

Why This Matters to Me

A passing synthetic test is not proof a real user can interact.

This is the failure mode that bites hardest — not the error you see, but the green that lies. CDP automation, jsdom, dispatched events — they all bypass computed pointer-events. They'll tell you it works while your users are tapping into nothing.

I've started running elementFromPoint checks before I close out any interactive UI build. Takes 10 seconds. Catches exactly this.

P.S. This isn't just a React Flow thing — the same isTrusted: false dynamic has burned me on MUI Autocomplete, React Router navigation, and dnd-kit drag interactions. The through-line is always the same: synthetic events can succeed where real pointer events fail. Verify at the pointer layer, not just the handler.