Layout Animations

Most powerful part of Motion, which allows to build animations between layout transitions. This allows to animate properties that weren't animatable before, like flex-direction and justify-content.

With layout animations, we directly change the style of the element, instead of using animate prop.

// https://codesandbox.io/p/sandbox/7r68g9

import { motion } from "framer-motion";
import { useState } from "react";
 
export default function Example() {
  const [open, setOpen] = useState(false);
 
  return (
    <div className="wrapper">
      <motion.div
        onClick={() => setOpen((t) => !t)}
        className="element"
        style={
          open
            ? { position: "fixed", inset: 0, width: "100%", height: "100%" }
            : { height: 48, width: 48 }
        }
        layout
      />
    </div>
  );
}

Shared Layout Animations

Connect two elements and create a smooth transition between them.

// https://codesandbox.io/p/sandbox/hm2prx

import { motion } from "framer-motion";
import { useState } from "react";
 
export default function Example() {
  const [showSecond, setShowSecond] = useState(false);
 
  return (
    <div className="wrapper">
      <motion.button
	      layout
	      className="button"
	      onClick={() => setShowSecond((s) => !s)}>
        Animate
      </motion.button>
      {showSecond ? (
        <motion.div
	        layoutId="rectangle"
	        className="second-element"
	        style={{ borderRadius: 12 }} />
      ) : (
        <motion.div
	        layoutId="rectangle"
	        className="element"
	        style={{ borderRadius: 12 }} />
      )}
    </div>
  );
}
  • By providing the same layoutId to both elements, they're connected to create a smooth transition.
  • By adding the layout prop to the button, it ensures a smooth movement for the button as well.
  • Without the custom borderRadius styles, the radius of the element gets distorted when the animation runs. When styles like borderRadius is changed, inlining the change is needed for detection.


Build this yourself on this CodeSandbox

Looking for the solution? Check this.

When you're animating whole layouts like above, every single element that goes through a transition needs to be connected via distinct layoutId values.

import { useEffect, useState, useRef } from "react";
import { useOnClickOutside } from "usehooks-ts";
import { motion, AnimatePresence } from "framer-motion";
 
export default function SharedLayout() {
  const [activeGame, setActiveGame] = useState(null);
  const ref = useRef(null);
  useOnClickOutside(ref, () => setActiveGame(null));
 
  useEffect(() => {
    function onKeyDown(event) {
      if (event.key === "Escape") {
        setActiveGame(null);
      }
    }
 
    window.addEventListener("keydown", onKeyDown);
    return () => window.removeEventListener("keydown", onKeyDown);
  }, []);
 
  return (
    <>
      <AnimatePresence>
        {activeGame ? (
          <motion.div
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            exit={{ opacity: 0 }}
            className="overlay"
          />
        ) : null}
      </AnimatePresence>
      <AnimatePresence>
        {activeGame ? (
          <div className="active-game">
            <motion.div
              layoutId={`card-${activeGame.title}`}
              className="inner"
              style={{ borderRadius: 12 }}
              ref={ref}
            >
              <div className="header">
                <motion.img
                  layoutId={`image-${activeGame.title}`}
                  height={56}
                  width={56}
                  alt="Game"
                  src={activeGame.image}
                  style={{ borderRadius: 12 }}
                />
                <div className="header-inner">
                  <div className="content-wrapper">
                    <motion.h2
                      layoutId={`title-${activeGame.title}`}
                      className="game-title"
                    >
                      {activeGame.title}
                    </motion.h2>
                    <motion.p
                      layoutId={`description-${activeGame.title}`}
                      className="game-description"
                    >
                      {activeGame.description}
                    </motion.p>
                  </div>
                  <motion.button
                    layoutId={`button-${activeGame.title}`}
                    className="button"
                  >
                    Get
                  </motion.button>
                </div>
              </div>
              <motion.p
                layout
                initial={{ opacity: 0 }}
                animate={{ opacity: 1 }}
                exit={{ opacity: 0, transition: { duration: 0.05 } }}
                className="long-description"
              >
                {activeGame.longDescription}
              </motion.p>
            </motion.div>
          </div>
        ) : null}
      </AnimatePresence>
      <ul className="list">
        {GAMES.map((game) => (
          <motion.li
            layoutId={`card-${game.title}`}
            key={game.title}
            onClick={() => setActiveGame(game)}
            style={{ borderRadius: 8 }}
          >
            <motion.img
              layoutId={`image-${game.title}`}
              height={56}
              width={56}
              alt="Game"
              src={game.image}
              style={{ borderRadius: 12 }}
            />
            <div className="game-wrapper">
              <div className="content-wrapper">
                <motion.h2
                  layoutId={`title-${game.title}`}
                  className="game-title"
                >
                  {game.title}
                </motion.h2>
                <motion.p
                  layoutId={`description-${game.title}`}
                  className="game-description"
                >
                  {game.description}
                </motion.p>
              </div>
              <motion.button
                layoutId={`button-${game.title}`}
                className="button"
              >
                Get
              </motion.button>
            </div>
          </motion.li>
        ))}
      </ul>
    </>
  );
}

Animating Height Changes

  • Framer allows to animate from a fixed height to auto. This is already not possible with plain CSS.
  • But its not possible to animate height from auto to auto; for example resize a modal with animation when the content inside changes.
  • To handle these cases, you need to define the height of the element manually, and observe changes using the ResizeObserver Web APIs|Web API.
  • There are also React hooks like useMeasure that are useful here.

Build this yourself on CodeSandbox Solution with useMeasure hook Solution with ResizeObserver API

import { motion } from "framer-motion";
import { useState, useRef, useEffect } from "react";
 
export default function Example() {
  const [showExtraContent, setShowExtraContent] = useState(false);
  const [height, setHeight] = useState(0);
  const elementRef = useRef(null);
 
  useEffect(() => {
    const observer = new ResizeObserver((entries) => {
      for (let entry of entries) {
        const rect = entry.target.getBoundingClientRect();
 
        setHeight(rect.height);
      }
    });
 
    if (elementRef.current) {
      observer.observe(elementRef.current);
    }
 
    return () => observer.disconnect();
  }, []);
 
  return (
    <div className="wrapper">
      <button className="button" onClick={() => setShowExtraContent((b) => !b)}>
        Toggle height
      </button>
      <motion.div animate={{ height }} className="element">
        <div ref={elementRef} className="inner">
          <h1>Fake Family Drawer</h1>
          <p>
            This is a fake family drawer. Animating height is tricky, but
            satisfying when it works.
          </p>
          {showExtraContent ? (
            <p>This extra content will change the height of the drawer. Some even more content to make the drawer taller and taller and taller...</p>
          ) : null}
        </div>
      </motion.div>
    </div>
  );
}
  • We can't put the ref and animate prop on the same element.
  • The element that contains the ref should have the full height we intend to animate to. For example, any padding must be applied to this element, not the motion.div.
  • With this configuration, we cannot get the correct height on initial render. It would be 0. If you want to avoid this layout shift, we can set the height to null when it's 0, which means that the height becomes auto on the initial render.
<motion.div
	animate={{
		height: height ? height : null
	}}
	className="element">