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
layoutIdto both elements, they're connected to create a smooth transition. - By adding the
layoutprop to the button, it ensures a smooth movement for the button as well. - Without the custom
borderRadiusstyles, the radius of the element gets distorted when the animation runs. When styles likeborderRadiusis 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
autotoauto; 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
ResizeObserverWeb 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
refandanimateprop on the same element. - The element that contains the
refshould have the full height we intend to animate to. For example, any padding must be applied to this element, not themotion.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
nullwhen it's0, which means that the height becomesautoon the initial render.
<motion.div
animate={{
height: height ? height : null
}}
className="element">