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.
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);
};
.mouse-tracker {
position: fixed;
pointer-events: none;
visibility: hidden;
// optional styles
}
You can use it like this (note that the offset is optional):
<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.
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:
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:
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: