0

I was trying to make a simple Button X & Y animation with framer motion, it transitions from element's point to another by interacting with it, just like Duolingo's word selection puzzles for example. The elements are also dynamically rendered, which I bind with dynamic refs.

I am confused as how to precisely get the certain element's X & Y coordinates on the DOM, I tried several solutions as to how to get it properly, but I am getting weird values which makes the origin of the animation very off.

  1. First try to get the elements X & Y coordinates
  2. Then set it on the states
  3. Pass it as props to the component
  4. Get the passed props as the starting point for the animation's initial prop

Here's a working sample sandbox that I made progress with https://codesandbox.io/s/zealous-pascal-6bgz2r

As you can see the source of elements' animation are off, it should start where the elements are clicked.

I tried getting the elements' X & Y coordinates from getBoundingClientRect by binding with react createRef hook, and clientX & clientY from Events. I also tried several options already such as getting the offsetTop or offsetLeft and some DOM computations, but still no luck.

I am now wondering if I am doing this correctly or there are some options on how to make an element transition to another place by interacting with it. Any suggestions or recommendations are welcome.

// WordSelectionList.tsx
import { createRef, useRef, useState } from "react";

import WordSelectedItem from "./WordSelectedItem";

const words = [
  "brown",
  "The",
  "fox",
  "dog",
  "over",
  "the",
  "jumps",
  "lazy",
  "quick",
];

const WordSelectionList = () => {
  const [selected, setSelected] = useState<string[]>([]);
  const [currX, setCurrX] = useState(0);
  const [currY, setCurrY] = useState(0);

  const refs = useRef<any[]>(words.map(() => createRef()));

  const handleWordSelect = (index: number, word: string) => {

    const element = refs.current[index].current;

    const clientRect = element.getBoundingClientRect();

    setCurrX(clientRect.x);
    setCurrY(clientRect.y);

    if (!selected.includes(word)) {
      setSelected((values) => [...values, word]);
    }
  };

  const handleWordUnSelect = (word: string) => {
    if (selected.includes(word)) {
      // remove the word from selected array
      setSelected((values) => values.filter((value) => value !== word));
    }
  };

  return (
    <div className="flex justify-center h-full">
      <div className="w-2/3 border p-6">
        <div className="flex flex-col space-y-4">
          <div className="flex border p-2">
            <div className="flex space-x-2 h-11">
              {selected.map((word, i) => (
                <WordSelectedItem
                  x={currX}
                  y={currY}
                  key={i}
                  word={word}
                  onSelect={handleWordUnSelect}
                />
              ))}
            </div>
          </div>
          <div className="flex gap-2">
            {words.map((word, i) => (
              <button
                ref={refs.current[i]}
                className="flex p-2 rounded-lg border"
                key={`word-${i}`}
                onClick={(e) => handleWordSelect(i, word)}
              >
                {word}
              </button>
            ))}
          </div>
        </div>
      </div>
    </div>
  );
};

export default WordSelectionList;
// WordSelectedItem.tsx
import { motion } from "framer-motion";

const WordSelectedItem = ({
  x,
  y,
  word,
  onSelect,
}: {
  x: number;
  y: number;
  word: string;
  onSelect: (word: string) => void;
}) => {
  return (
    <motion.button
      className="flex border rounded-lg p-2"
      initial={{ x, y }}
      animate={{ x: 0, y: 0 }}
      transition={{ duration: 1.5 }}
      onClick={() => onSelect(word)}
    >
      <span>{word}</span>
    </motion.button>
  );
};

export default WordSelectedItem;
Lemon
  • 1
  • 1

0 Answers0