JoshWComeau

useMousePosition

Filed under
Snippets
on
in
March 20th, 2022.
Mar 2022.
import React from 'react';

const useMousePosition = () => {
  const [
    mousePosition,
    setMousePosition
  ] = React.useState({ x: null, y: null });

  React.useEffect(() => {
    const updateMousePosition = ev => {
      setMousePosition({ x: ev.clientX, y: ev.clientY });
    };

    window.addEventListener('mousemove', updateMousePosition);

    return () => {
      window.removeEventListener('mousemove', updateMousePosition);
    };
  }, []);

  return mousePosition;
};

export default useMousePosition;

For certain interactions, you need to know exactly where the user's cursor is.

For example, on this blog, I have a “like” button which responds to the user's cursor position:

useMousePosition is a low-level hook used in effects like these. It measures the user's current mouse position, in pixels, from the top/left corner. It stores this data in React state, and updates it whenever the cursor moves.

Because it's held in React state, the component will re-render whenever the user moves the mouse, and so you can safely use it to calculate things like CSS transform values, canvas animations, etc.

Link to this headingExample usage:

Code Playground

JSX

Result

Link to this headingPerformance

This component will re-render whenever the user moves the mouse. This can be dozens and dozens of times a second.

Originally, this hook included “throttle” functionality, which would limit the updates to a user-specified interval. In testing, though, it seemed to make performance worse. No matter how hard I tried, I couldn't come up with a contrived scenario where the throttle actually improved performance (while still updating often enough for smooth animations).

That said, you do still need to be a bit careful where you use this hook. It shouldn't be used in a top-level component like App or Homepage, since that will cause a huge chunk of your React tree to re-render very often. Use this hook in the small “leaf node” components near the bottom of the tree.

For maximum performance, you can use a library like React Spring or Framer Motion, which will allow you to update values without triggering React renders. In my experience, though, as long as you're using this hook on smaller components that don't have a big DOM impact, you should be just fine.

Link to this headingAccessibility

If you're going to be using this hook to animate an element's position, be sure to consider the user's motion preferences!

You can check with the use-prefers-reduced-motion hook. For example:

function CursorBox() {
  const mousePosition = useMousePosition();
  const prefersReducedMotion = usePrefersReducedMotion();

  const transform = prefersReducedMotion
    ? null
    : `translate(${mousePosition.x}px)`;

  return (
    <div
      className="cursor-box"
      style={{
        transform,
      }}
    />
  );
}

Link to this headingTouchscreens and mobile

This hook is written to respond exclusively to mouse events. This means that it'll ignore the movement of fingers across a touchscreen.

In my experience, I generally don't want to handle touch events in the same way. Dragging a finger across the screen is more like scrolling than it is like moving a mouse.

I mainly use this hook for cosmetic desktop-only effects, like the 404 error page(opens in new tab) on this blog.

For those curious, though, here's how I'd write this hook if it needed to also track touch position:

import React from 'react';

const useMousePosition = ({ includeTouch }) => {
  const [
    mousePosition,
    setMousePosition
  ] = React.useState({ x: null, y: null });

  React.useEffect(() => {
    const updateMousePosition = ev => {
      let x, y;

      if (ev.touches) {
        const touch = ev.touches[0];
        [x, y] = [touch.clientX, touch.clientY];
      } else {
        [x, y] = [ev.clientX, ev.clientY];
      }

      setMousePosition({ x, y });
    };

    window.addEventListener('mousemove', updateMousePosition);

    if (includeTouch) {
      window.addEventListener('touchmove', updateMousePosition);
    }

    return () => {
      window.removeEventListener('mousemove', updateMousePosition);

      if (includeTouch) {
        window.removeEventListener('touchmove', updateMousePosition);
      }
    };
  }, [includeTouch]);

  return mousePosition;
};

export default useMousePosition;

Last updated on

March 20th, 2022

# of hits