Search code examples
reactjsanimationframer-motion

Framer Motion: Fix child distortion when animated with AnimateSharedLayout


I want to crossfade a card (div with children) with AnimateSharedLayout when it gets expanded.

However, the font gets distorted on the layout animation.

Here is a link to a codesandbox.

Here is my code (I am using Tailwind CSS):

This is the Projects component that holds all the cards in a CSS grid:

export default function Projects() {
  const projectObjArray = [
    {
      title: "Project 1",
      description: "some description",
      background: "#f00",
      link: ""
    },
    {
      title: "Project 2",
      description: "some description",
      background: "#f00",
      link: ""
    },
    {
      title: "Project 3",
      description: "some description",
      background: "#f00",
      link: ""
    }
  ];

  const [expanded, setExpanded] = useState(undefined);

  return (
    <div>
      <h1 className="text-6xl mb-5 md:text-left md:ml-10 pb-5">My Projects</h1>
      <div className="grid grid-cols-1 md:grid-cols-3 gap-10">
        {projectObjArray.map((el, index) => {
          if (index !== expanded) {
            return (
              <ProjectTemplate
                projectobj={el}
                key={el.title}
                index={index}
                setexpanded={setExpanded}
              />
            );
          } else {
            return (
              <ProjectTemplateExpanded
                projectobj={el}
                key={el.title}
                index={index}
                setexpanded={setExpanded}
              />
            );
          }
        })}
      </div>
    </div>
  );
}

When a card is clicked, its index gets saved in the expanded state. Then, the ProjectTemplateExpanded component will render at that position instead of the ProjectTemplate.

Here are these components:

export default function ProjectTemplate(props) {
  return (
    <motion.div
      initial={{ borderRadius: "100px" }}
      animate={{ borderRadius: "50px" }}
      className="w-full h-56 flex flex-col justify-center"
      style={{ background: props.projectobj.background }}
      onClick={() => {
        props.setexpanded(props.index);
      }}
      layoutId={`Container${props.projectobj.title}`}
      transition={{ duration: 2 }}
    >
      <motion.h1
        layoutId={`Title${props.projectobj.title}`}
        // layout
        transition={{ duration: 2 }}
        className="text-2xl"
      >
        {props.projectobj.title}
      </motion.h1>
      <motion.p
        layoutId={`Description${props.projectobj.title}`}
        // layout
        transition={{ duration: 2 }}
        className="text-1xl"
      >
        {props.projectobj.description}
      </motion.p>
    </motion.div>
  );
}
export default function ProjectTemplateExpanded(props) {
  return (
    <>
      <div className="h-56"></div>
      <motion.div
        className="w-full fixed top-0 left-0 right-0 bottom-0 z-50 flex flex-col justify-center items-center"
        style={{ background: "rgba(0, 0, 0, 0.5)" }}
        onClick={() => {
          props.setexpanded(undefined);
        }}
      >
        <motion.div
          layoutId={`Container${props.projectobj.title}`}
          transition={{ duration: 2 }}
          initial={{ borderRadius: "50px" }}
          animate={{ borderRadius: "100px" }}
          style={{
            background: props.projectobj.background,
            width: "80vw",
            height: "50vh"
          }}
          onClick={(e) => {
            e.stopPropagation();
          }}
          className="flex flex-col justify-center"
        >
          <motion.h1
            layoutId={`Title${props.projectobj.title}`}
            // layout
            transition={{ duration: 2 }}
            className="text-2xl"
          >
            {props.projectobj.title}
          </motion.h1>
          <motion.p
            layoutId={`Description${props.projectobj.title}`}
            // layout
            transition={{ duration: 2 }}
            className="text-1xl"
          >
            {props.projectobj.description}
          </motion.p>
        </motion.div>
      </motion.div>
    </>
  );
}

The Projects component is wrapped with an AnimateSharedLayout.

export default function App() {
  return (
    <div className="App text-white pt-5 px-5">
      <AnimateSharedLayout type="crossfade">
        <Projects />
      </AnimateSharedLayout>
    </div>
  );
}

The documentation states:

To correct distortion on immediate children, add layout to those too.

So I've tried adding the layout prop beside the layoutId on the h1 and the p tag, but that didn't change anything.

This blog post states that scale correction gets applied on any component that takes part in a shared element transition (that's the case here, because of the layoutId set on the children)

What am I missing?


Solution

  • The solution is to put items-center on the container. Then, the width of the h1 and p elements will get corrected during the animation.