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.
- First try to get the elements X & Y coordinates
- Then set it on the states
- Pass it as props to the component
- 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;