<MouseTracker/>

A react component that follows your mouse

This component lets you render content that follows your mouse.

The component accepts any children, and renders them into a fixed-position div that takes the coordinates of the mouse, with optional offsets.

The element is rendered into the body using createPortal(), in order to avoid any overflow issues.

jsx
const MouseTracker = ({ children, offset = { x: 0, y: 0} }) => {
    const element = useRef({});

    useEffect(() => {
        function handler(e) {
            if (element.current) {
                const x = e.clientX + offset.x, y = e.clientY + offset.y;
                element.current.style.transform = `translate(${x}px, ${y}px)`;
                element.current.style.visibility = 'visible';
            }
        }
        document.addEventListener('mousemove', handler);
        return () => document.removeEventListener('mousemove', handler);
    }, [offset.x, offset.y]);

    return createPortal(
        <div className='mouse-tracker' ref={element}>
            {children}
        </div>
    , document.body);
};
css
.mouse-tracker {
    position: fixed;
    pointer-events: none;
    visibility: hidden;
    // optional styles
}

You can use it like this (note that the offset is optional):

jsx
<MouseTracker offset={{ x: 20, y: 20 }}>Some Text</MouseTracker>

Performance considerations

State vs. Ref

For performance reasons, I chose to apply the styles manually via a ref instead of using a state. I prefer to avoid React's rendering cycle for animations that I want to be as smooth as possible.

requestAnimationFrame()

I've also considered using requestAnimationFrame(), since it's generally a good idea to use that for animations, especially when you need to synchronize the animation with the browser's rendering cycle (e.g. when different elements should move in sync).

However, in this case, I found that it's not necessary, since the cursor is not rendered by the browser, and therefore there's no reason to sync the animation with the browser's rendering cycle.

I've also found that the mousemove event is fired at the same rate as the animation frame, making requestAnimationFrame() redundant.

Transforms vs. top/left

The initial implementation used top and left to position the element, but using transform is more efficient, since it doesn't trigger a reflow. Moreover, in most browsers, transform is hardware-accelerated (running on the GPU), making it even more efficient.

Thanks billybobjobo for pointing this out.

Implementation with useDocumentEvent()

We can simplify the implementation by using the useDocumentEvent hook, which is a custom hook that takes care of adding and removing event listeners on the document.

jsx
const MouseTracker = ({ children, offset = { x: 0, y: 0} }) => {
    const element = useRef({});
    useDocumentEvent('mousemove', (e) => {
        if (element.current) {
            const x = e.clientX + offset.x, y = e.clientY + offset.y;
            element.current.style.transform = `translate(${x}px, ${y}px)`;
            element.current.style.visibility = 'visible';
        }
    });

    return createPortal(
        <div className='mouse-tracker' ref={element}>
            {children}
        </div>
    , document.body);
};

Mobile Support

Pointer device tracking elements are more suitable to desktop devices, since dragging your finger on a mobile device is usually a scroll (or drag) gesture.

However, if you want this component to support touch devices, you can do so by adding a touchmove event listener and using the first touch point's coordinates:

jsx
const MouseTracker = ({ children, offset = { x: 0, y: 0} }) => {
    const element = useRef({});

    useEffect(() => {
        function handler(ev) {
            if (element.current) {
                const e = ev.touches ? ev.touches[0] : ev;
                const x = e.clientX + offset.x, y = e.clientY + offset.y;
                element.current.style.transform = `translate(${x}px, ${y}px)`;
                element.current.style.visibility = 'visible';
            }
        }
        document.addEventListener('mousemove', handler);
        document.addEventListener('touchmove', handler);
        return () => {
            document.removeEventListener('mousemove', handler);
            document.removeEventListener('touchmove', handler);
        }
    }, [offset.x, offset.y]);

    return createPortal(
        <div className='mouse-tracker' ref={element}>
            {children}
        </div>
    , document.body);
};

Examples

Here's a simple example where we render some text that follows the mouse:

Code Playground

In the next example, we add a state to conditionally render the <MouseTracker/> and also change its contents based on the element that the mouse is currently hovering over:

Code Playground
Don't keep this brilliance to yourself.

A newsletter for front-end web developers

Stay up-to-date with my latest articles, experiments, tools, and much more!

Issued monthly (or so). No spam. Unsubscribe any time.