JoshWComeau

useBoop

Filed under
Snippets
on
in
November 23rd, 2020.
Nov 2020.
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!

Last updated on

November 23rd, 2020

# of hits