Why I Rejected Cursor's 'Correct' Z-Index Fix

4 min read
Last updated:

I currently work with a team building a workflow builder and recently worked on a feature where users can attach notes to nodes. If a node has a note, hovering it shows that note in a tooltip. Users can also pin one or more tooltips so the note stays visible while they keep working.

After the first iteration, we noticed that the implementation worked fine until a pinned tooltip was over other panels outside the canvas (node editor and node library in the demo video). The pinned tooltip could render on top of these panels even when the node itself was visually under a panel.

The video below demonstrates the bug where pinned tooltips incorrectly render above UI panels. The real app has more panels but we show only two to demonstrate the bug.

Pinned tooltip rendering above panels (node editor and node library) even when the node is visually under them.

When we caught this in review, my first thought was: “AI can probably fix this quickly.” So I asked Cursor to fix it.

It did, but the solution felt off.

The solution it came up with was: give every section outside the canvas a z-index that’s “the tooltip z-index + 1” so the tooltip ends up underneath. It works, but it comes with two problems:

  • When we add a new UI panel outside the canvas, we have to remember to assign the correct z-index again.
  • You’re one step closer to z-index wars--where stacking becomes a fragile pile of numbers nobody wants to touch.

Looking at the solution, it felt like Cursor was treating a symptom instead of fixing the root cause. Given that I know a few things about CSS, I could tell that this is a typical stacking context problem.

When I looked at the code, I found out that the tooltips were being rendered in document.body using a React portal. This meant the tooltips are no longer inside the canvas stacking context, so they can escape and overlap UI that should sit above the canvas.

When I looked at how React Flow organizes its layers, rendering the tooltips inside the nodes container in the canvas instead of the document body was a better fix because it:

  • Keeps the tooltips in the correct stacking context so they can't leak above any UI elements outside the canvas
  • Removes the need to assign z-index values across the entire app to compensate for the tooltip.
  • Was low effort, Z-index tweaking touched 7 files whereas the stacking context fix touched 1 file.

Let's see a quick comparison of the code changes:

tsx
// The change Cursor did in 7 files for all panels outside the canvas.
import styled from "styled-components";
import { Z_INDEX } from "@/app/constants";

const StyledToolBarContainer = styled.div`
  z-index: ${Z_INDEX.FLOW_EDITOR.CANVAS_NODE_TOOLTIP + 1};
`;
tsx
// The change I only did in the canvas tooltip file.
import { createPortal } from "react";

const NodeTooltip = () => {
  const container =
    document.querySelector(".react-flow__nodes") ?? document.body;

  return createPortal(<Tooltip />, container);
};

If I could pinpoint a few things I carried away from this on AI-assisted code:

  • Correctness is not the same as maintainability.
  • When fixing bugs, symptoms are different from root causes.
  • You are shipping a solution that future maintainers and reviewers have to live with. Even if these might be AI agents in the future, they are good at replicating patterns--so leave good patterns.

AI is a great accelerator but doesn't replace judgement. Before you push an AI-generated diff, it's worth scanning it for long-term costs like maintainability, reviewability and how the solution will scale when the UI grows. On solo projects, you can accept "good enough" but when working on a team you have to think about how a solution affects your team members too.

What started as a multiplayer prototype revealed duplicated state, overloaded slices, and blurred team ownership. This is how we refactored it — and why reading the docs still matters when coding with AI.

We needed to solve concurrent editing in our React Flow workflow builder. Here's how Yjs + WebSockets, Yjs + SSE, and Liveblocks compared — and why we chose locks instead.

Discover how git worktrees let you work on multiple branches simultaneously without the hassle of git stash—perfect for juggling features, hotfixes, and code reviews.