From 2880eee63ce69dca4453010d4fd815fe609c5e0c Mon Sep 17 00:00:00 2001 From: Francesco Sansalvadore Date: Mon, 6 Jul 2026 13:49:11 +0200 Subject: [PATCH] fix(www): stop Realtime card peer cursors from breaking the hover animation (#47631) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Bug fix. ## What is the current behavior? On the `apps/www` homepage, the Realtime product card shows a cursor-following hover animation (the "self" cursor) alongside two other peer cursors (User 1 and User 2) that animate independently. The mouse-follow animation is driven by an `onMouseMove` handler on the `
` in `RealtimeVisual.tsx`. The three animated cursor divs are positioned absolutely on top of that figure but did **not** set `pointer-events-none`. As the peer cursors move around (and the self cursor tracks the pointer), they become the pointer target under the OS cursor and intercept mouse events, so the main hover animation stutters/breaks when the pointer passes over one of the other cursors. ## What is the new behavior? Added `pointer-events-none` to all three animated cursor wrappers (User 1, User 2, and Self). The animated cursors are purely decorative, so they should never intercept pointer events. The `
` keeps `pointer-events-auto` and now receives `onMouseMove` uninterrupted, so the cursor-follow animation stays smooth while hovering over the card. ## Additional context Three-line change in `apps/www/components/Products/RealtimeVisual.tsx`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01YCXXskLSt6k4DjUxgS4vc5 --- _Generated by [Claude Code](https://claude.ai/code/session_01YCXXskLSt6k4DjUxgS4vc5)_ ## Summary by CodeRabbit - **Bug Fixes** - Improved interaction behavior for the on-screen user indicators so they no longer capture pointer events, ensuring hover/drag behavior elsewhere remains consistent. - Refined the pointer-follow animation wiring to use DOM event listeners for more reliable coordinate tracking. - Updated motion behavior to respect “reduce motion” settings by disabling the pointer-follow effect when enabled. - Kept “Self” marker hover opacity transitions consistent with previous visuals. --------- Co-authored-by: Claude --- .../components/Products/RealtimeVisual.tsx | 54 +++++++++++++------ 1 file changed, 37 insertions(+), 17 deletions(-) diff --git a/apps/www/components/Products/RealtimeVisual.tsx b/apps/www/components/Products/RealtimeVisual.tsx index cf6f783fb7c..64b3d622d98 100644 --- a/apps/www/components/Products/RealtimeVisual.tsx +++ b/apps/www/components/Products/RealtimeVisual.tsx @@ -1,6 +1,6 @@ import { isBrowser, useReducedMotion } from 'common' import Image from 'next/image' -import React, { useRef, useState } from 'react' +import React, { useEffect, useRef, useState } from 'react' import { cn } from 'ui' interface Props { @@ -8,15 +8,29 @@ interface Props { } const RealtimeVisual: React.FC = ({ className }) => { - const cardRef = useRef(null) + const figureRef = useRef(null) const [svgTransformSelf, setSvgTransformSelf] = useState('translate(0px, 0px)') const [svgTransform, setSvgTransform] = useState('translate(0px, 0px)') const [svgTransform2, setSvgTransform2] = useState('translate(0px, 0px)') const reduceMotion = isBrowser && useReducedMotion() - const handleMouseMove = (event: React.MouseEvent) => { - if (cardRef.current) { - const cardRect = cardRef.current.getBoundingClientRect() + useEffect(() => { + if (reduceMotion) return + + const figure = figureRef.current + if (!figure) return + + // Track the pointer across the whole card (the Link ancestor), not just the + // figure. The card's text content sits above the figure at a higher z-index, + // and it is a sibling of the figure rather than a descendant, so listening on + // the figure alone means the animation freezes whenever the pointer is over + // the text or other overlapping elements. Listening on the card lets the + // mousemove events bubble up from any child regardless of stacking order. + const card = figure.closest('a') ?? figure + + const handleMouseMove = (event: MouseEvent) => { + // Coordinates stay relative to the figure so the animation math is unchanged. + const cardRect = figure.getBoundingClientRect() const mouseX = event.clientX - cardRect.left // Mouse X relative to card const mouseY = event.clientY - cardRect.top // Mouse Y relative to card const cardWidth = cardRect.width @@ -34,24 +48,30 @@ const RealtimeVisual: React.FC = ({ className }) => { ) setSvgTransformSelf(`translate(${mouseX + 12}px, ${mouseY + 4}px)`) } - } - const handleMouseLeave = () => { - setSvgTransform('translate(0px, 0px)') - setSvgTransform2('translate(0px, 0px)') - } + const handleMouseLeave = () => { + setSvgTransform('translate(0px, 0px)') + setSvgTransform2('translate(0px, 0px)') + } + + card.addEventListener('mousemove', handleMouseMove) + card.addEventListener('mouseleave', handleMouseLeave) + + return () => { + card.removeEventListener('mousemove', handleMouseMove) + card.removeEventListener('mouseleave', handleMouseLeave) + } + }, [reduceMotion]) return (
= ({ className }) => { /> {/* User 1 */}
= ({ className }) => {
{/* User 2 */}
= ({ className }) => {
{/* Self */}