Skip to content

Commit 7908932

Browse files
committed
fix(ux): HelpModal backdrop flicker on mousemove over canvas
User-reported (screenshot 2026-05-23 20:22): clicking '?' on a brick in the left Palette opens the HelpModal; moving the cursor right across the canvas underneath caused the whole screen to flicker. Root cause: HelpModal backdrop used backdropFilter:blur(8px) on a position:fixed inset:0 overlay. Chrome was forced to recompose the GPU backdrop layer on every mousemove event because the React Flow canvas underneath schedules per-frame cursor-tracking repaints — each canvas paint invalidated the backdrop, which then forced a fresh blur pass, looping at frame rate. Fix: - Drop backdropFilter:blur entirely; keep the rgba(15,23,42,0.55) flat scrim — still visually distinct enough to anchor focus. - Add transform: translateZ(0) + willChange: opacity to promote the backdrop to its own compositing layer, isolating it from the canvas paint underneath. 1 new vitest pins the contract that backdropFilter is empty and transform contains translateZ — any future re-introduction of blur will trip this test.
1 parent 512d634 commit 7908932

2 files changed

Lines changed: 24 additions & 2 deletions

File tree

vbgui/src/components/HelpIcon.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -921,8 +921,16 @@ export function HelpModal({ topic, onClose }: HelpModalProps): JSX.Element {
921921
style={{
922922
position: "fixed",
923923
inset: 0,
924-
background: "rgba(15, 23, 42, 0.75)",
925-
backdropFilter: "blur(8px)",
924+
// UX-fix: backdropFilter: blur(8px) forced Chrome to recompose
925+
// the GPU layer on every mousemove (cursor crossing the
926+
// overlay → repaint loop with React Flow canvas underneath
927+
// → visible flicker). Plain rgba(0,0,0,0.5) keeps focus on
928+
// the modal without forcing per-frame backdrop recomposite.
929+
background: "rgba(15, 23, 42, 0.55)",
930+
// Promote to its own compositing layer so mousemove over the
931+
// backdrop doesn't invalidate the canvas paint underneath.
932+
transform: "translateZ(0)",
933+
willChange: "opacity",
926934
display: "flex",
927935
alignItems: "center",
928936
justifyContent: "center",

vbgui/tests/HelpIcon.test.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,18 @@ describe("HelpIcon + HelpModal", () => {
4040
fireEvent.click(screen.getByTestId("help-modal-close"));
4141
expect(screen.queryByTestId("help-modal-dim_env_nh")).toBeNull();
4242
});
43+
44+
// UX-fix: prior implementation set backdropFilter:blur(8px) on the
45+
// fixed inset:0 overlay. Chrome recomposed the GPU layer on every
46+
// mousemove that crossed the overlay → visible flicker over the
47+
// React Flow canvas underneath. Fix drops the blur and promotes
48+
// the backdrop to its own compositing layer via translateZ(0).
49+
it("backdrop has no backdropFilter:blur (anti-flicker fix)", () => {
50+
render(<HelpIcon topic="dim_env_H" />);
51+
fireEvent.click(screen.getByTestId("help-icon-dim_env_H"));
52+
const backdrop = screen.getByTestId("help-modal-backdrop") as
53+
HTMLElement;
54+
expect(backdrop.style.backdropFilter || "").toBe("");
55+
expect(backdrop.style.transform).toContain("translateZ");
56+
});
4357
});

0 commit comments

Comments
 (0)