JoshWComeau

useBoop

Filed under
Snippets
on
in
November 23rd, 2020.
Nov 2020.
Last updated
on
in
July 16th, 2025.
Jul 2025.
import React from 'react';
import { useSpring } from 'react-spring';

// UPDATE this path to your copy of the hook!
// Source here: https://joshwcomeau.com/snippets/react-hooks/use-prefers-reduced-motion
import usePrefersReducedMotion from '@/hooks/use-prefers-reduced-motion';

function useBoop({
  x = 0,
  y = 0,
  rotation = 0,
  scale = 1,
  timing = 150,
  springConfig = {
    tension: 300,
    friction: 10,
  },
}) {
  const prefersReducedMotion = usePrefersReducedMotion();

  const [isBooped, setIsBooped] = React.useState(false);

  const style = useSpring({
    transform: isBooped
      ? `translate(${x}px, ${y}px)
         rotate(${rotation}deg)
         scale(${scale})`
      : `translate(0px, 0px)
         rotate(0deg)
         scale(1)`,
    config: springConfig,
  });

  React.useEffect(() => {
    if (!isBooped) {
      return;
    }

    const timeoutId = window.setTimeout(() => {
      setIsBooped(false);
    }, timing);

    return () => {
      window.clearTimeout(timeoutId);
    };
  }, [isBooped]);

  const trigger = React.useCallback(() => {
    setIsBooped(true);
  }, []);

  let appliedStyle = prefersReducedMotion ? {} : style;

  return [appliedStyle, trigger];
}

export default useBoop;

This hook is described in much more detail in my tutorial, Boop: A whimsical twist on hover transitions.

In order for it to work, you'll also need to grab another hook, usePrefersReducedMotion.

You can build a thin component wrapper over it, for cases where the trigger and animation happen on the same element:

// components/Boop.jsx
import React from 'react';
import { animated } from 'react-spring';

import useBoop from '@/hooks/use-boop';

const Boop = ({ children, ...boopConfig }) => {
  const [style, trigger] = useBoop(boopConfig);

  return (
    <animated.span onMouseEnter={trigger} style={style}>
      {children}
    </animated.span>
  );
};

Can't wait to see what you come up with!

This snippet, along with any other code on this page, is released to the public domain under the Creative Commons Zero (CC0) license(opens in new tab).

Last updated on

July 16th, 2025

# of hits