reactjsframerjsframer-motion

Combine dragging and animating drag position on click (animate x.set())


For a client we're building horizontally dragging rows of media items. Dragging horizontally using Framer-Motion works great, but I can't animate the x position on the click of a button.

This is the general idea:

enter image description here

This is the component as I currently have it (I removed style, etc. for brevity):

const HorizontalScroll = ({ children }) => {
  const x = useMotionValue(0);

  function onLeftClick() {
    const xPos = x.get();

    if (Math.round(xPos) === 0) {
      return;
    }

    const newXPosition = xPos + 600;

    x.set(newXPosition > 0 ? 0 : newXPosition);
  }

  function onRightClick() {
    const xPos = x.get();

    const newXPosition = xPos - 600;

    x.set(newXPosition < -2000 ? -2000 : newXPosition);
  }

  return (
    <>
      <button
        type="button"
        onClick={onLeftClick}
      >
        Left
      </button>

      <motion.div
        drag="x"
        dragConstraints={{ left: -2000, right: 0 }}
        initial={false}
        style={{ width: 2000, x }}
      >
        {children}
      </motion.div>

      <button
        type="button"
        onClick={onRightClick}
      >
        Right
      </button>
    </>
  );
};

So I do x.set() when clicking either left or right on the arrows. Doing x.set() works, but it isn't animated.

Instead of const x = useMotionValue(0) I could use useSpring or useTransform, but that breaks the dragging behaviour.

So in short, I want to animate the x.set(), but I've no idea how to do that. Anybody got an idea?


Solution

  • I finally posted my question and I find the answer to my own question in under an hour...

    In case anyone has the same edge case question. What I came up with (and this is by no means the most elegant solution) is using useAnimation. My component currently looks like this:

    const translateXForElement = (element) => {
      const transform = element.style.transform;
    
      if (!transform || transform.indexOf('translateX(') < 0) {
        return 0;
      }
    
      const extractTranslateX = transform.match(/translateX\((-?\d+)/);
    
      return extractTranslateX && extractTranslateX.length === 2
        ? parseInt(extractTranslateX[1], 10)
        : 0;
    }
    
    const HorizontalScroll = ({ children }) => {
      const dragRef = useRef(null);
      const animation = useAnimation();
    
      function onLeftClick() {
        const xPos = translateXForElement(dragRef.current);
        const newXPosition = xPos + 600;
    
        animation.start({
          x: newXPosition > 0 ? 0 : newXPosition,
        });
      }
    
      function onRightClick() {
        const xPos = translateXForElement(dragRef.current);
        const newXPosition = xPos - 600;
    
        animation.start({
          x: newXPosition < -2000 ? -2000 : newXPosition,
        });
      }
    
      return (
        <>
          <button
            type="button"
            onClick={onLeftClick}
          >
            Left
          </button>
    
          <motion.div
            drag="x"
            dragConstraints={{ left: -2000, right: 0 }}
            initial={false}
            animate={animation}
            style={{ width: 2000, x: 0, opacity: 1 }}
            ref={dragRef}
          >
            {children}
          </motion.div>
    
          <button
            type="button"
            onClick={onRightClick}
          >
            Right
          </button>
        </>
      );
    };
    

    I couldn't find a (nice) solution to retrieve the current translateX for an element, so I did a regex on element.style.transform for now. It's too bad useAnimation() doesn't allow retrieving the x (or I'm missing something).

    Of course if you've anything to approve in my code, I would love to hear it!