Shots in this scene
Layout Animations
In one line
Add layout and Motion animates an element between two layouts using only transforms; add layoutId and an element can travel between two places. Transforms stretch content, so children and radii need the fixes in this topic.
Think of it like this
Moving house with a photo of each room. Before the move you photograph the living room; after, you photograph it again. You do not slide the furniture one inch at a time. You place it where it was in the first photo, then let it drift to where it is in the second. layout does this: measure, invert, play (Phase 10's FLIP, done for you).
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- layout
- A prop that makes an element animate to its new size and position whenever React re-renders it, using transforms.
- layoutId
- A name that links two elements, so one animates from where the other was. The two should not exist at the same time.
- scale distortion
- The stretching of an element's children, text and rounded corners when the parent is animated with
scale. - shared element
- One visual thing that seems to travel between two places, such as a tab underline.
- LayoutGroup
- A wrapper that makes layout animations in its children coordinate, and that can namespace
layoutIds with anid. - layoutScroll
- A prop for scrollable parents, so layout animations inside them account for the scroll offset.
Step by step
01Resize the chip
Run the first example and press Resize with the Timeline paused. The chip is an orange pill that grows from 70 to 220 px. Slow it to 0.1× and look at the chip's first frames: its transform starts at scale(0.32, 1) (its old size), and the word Volt is squeezed to 8 px wide.
02Counter-scale the child
Press Child gets layout so it says on, then Resize again. The word keeps its width of 25 px during the whole move. Its own transform is the inverse of the chip's.
03Glide a shared element
Run the second example. Bar A and B wrap their tabs in LayoutGroup; bars C and D do not. The page presses the second tab of B and D. Slow the clock to 0.1× to catch D's underline arriving from C's bar above.
04Keep ids apart
Wrap each tab bar in <LayoutGroup id="..."> with a different id, as A and B do. The underline now stays in its own bar.
Try it yourself
- 1
Move the radius out of style
Delete
style={{ borderRadius: 22 }}and add.chip { border-radius: 22px; }in the CSS. Resize with the clock paused and step: the pill's ends are squashed ellipses during the move, because Motion cannot correct a radius it does not know about. Measured:style.borderRadiusstays empty. - 2
Compare the child fixes
Set the child's layout prop to
true,"position"andfalsein turn. Measured widths ofVoltat 20 ms into the move: 25 px, 25 px and 8 px.positionalso keeps the word's own size changes from animating.
Code & diagrams
Resize runs a layout animation. With the child's layout switch off, the word is squashed during the move; switch it on and it keeps its width. Look at the pill's rounded ends: its radius is in `style`, so Motion corrects it.
Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.
Console
Nothing logged yet.The page presses the second tab on bars B and D. B (in a LayoutGroup) glides within its bar; D, sharing the global id with C, slides in from C's bar.
Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.
Console
Nothing logged yet.Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Share one layoutId between two tab bars
In the second example remove <LayoutGroup id="two"> around bar B and press the second tab on both B and D.
Myth vs fact
Myth
layout animates width and height.
Fact
It animates a transform between the old and new box. The element is already at its final size; the transform makes it look like it is not, so content can be stretched (distortion).
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Layout animations are the one place where Motion's projection tree matters: each
layoutelement joins a tree with itsmotionancestors, and a parent's transform is divided out of each child's. That is howlayouton the child counter-scales: the child projects against a parent that is itself being scaled. - ▸
A
layoutanimation continues from the box the element is at now. Press Resize twice, 60 ms apart: measured, the chip was 76 px wide at 259 ms and 76 px at 261 ms, just after the reversal, with no jump. - ▸
layoutIdalso crossfades the two elements (the old fades out while the new fades in as the box moves), which suits shared elements with different content, such as a thumbnail and a large image.
Remember this
- 1
The problem this solves: an element that changes
width,justify-contentor its place in a list jumps to its new layout in one frame. Animatingwidthortopevery frame costs layout and paint (Phase 6). Phase 10 built FLIP by hand withgetBoundingClientRect;layoutmeasures before and after every render for you, then animates atransformfrom the old box to the new one. The React course (Topic 11.6,/react) shows the basic call; here is how it behaves. - 2
What runs. Motion writes the transform into the element's
styleeach frame from JavaScript. There is no WAAPI animation, so the Timeline shows no track (measured: the style attribute heldtranslate3d(-74.9px, 0px, 0px) scale(0.319, 1)30 ms after a 220 px chip shrank to 70 px, and 70 / 220 is 0.318). Interrupting continues from the current box. - 3
Scale distortion. The chip above is animated with
scale, so its content scales too. Text in a child with nolayoutis squashed: measured width of the wordVoltwas 8 px at the start of the move (its natural width is 25 px). Fix one: give the childlayouttoo, and Motion counter-scales it (25 px, constant, measured). Fix two:layout="position"on the child animates only its position, never its size (also 25 px, measured). Fix three, for borders: setborderRadiusandboxShadowinstyleoranimate, not in CSS. Motion corrects those two (measured: the radius instylewent from 31.3% to 22.3% during the move). A radius written in a stylesheet is not corrected: measured withborder-radius: 22pxin CSS,style.borderRadiusstayed empty while the chip was squashed to 0.32 wide. - 4
**Shared elements with
layoutId.** Give two elements the samelayoutId, and render only one at a time. When one disappears and the other appears, Motion animates the new one from the old one's box, with a crossfade. A tab underline is the classic case:{selected && <motion.span layoutId="underline" />}in every tab. The configurator uses it for the colour ring. - 5
**
LayoutGroup.** Layout ids are global to the page. Two separate tab bars that both uselayoutId="underline"collide: pressing the second tab of the second bar started its underline 29 px above, flying in from the other bar (measured, on bars 29 px apart), while the bar wrapped in<LayoutGroup id="two">did not move from its own place.LayoutGroup idnamespaces the ids. Without anid,LayoutGroupmakes siblings re-measure together: a layout change in one accordion makes its neighbours animate to their new place, too. - 6
Edge cases. A scrollable ancestor shifts what the browser measures; Motion's source subtracts the scroll of ancestors marked
layoutScroll, so put that prop on scroll containers (andlayoutRooton a fixed-position one). Everylayoutelement is measured whenever its tree renders, so a very large list gets slow;layoutDependencylimits measuring to when a value you name changes. Under reduced motion layout animations become instant (Topic 15.6).
Explain it without notes
Why does layout stretch an element's text, and what are three fixes?
What is layoutId, and when do you need LayoutGroup?
Practice
Make a card grid where clicking a card toggles it between small and large, with its title not stretching.
Add a highlight behind the active item of a vertical menu that glides between items.
Done when you can
You can add
layoutand say what runs underneathYou can name three fixes for scale distortion and apply them
You can build a shared element with
layoutIdand keep ids apart withLayoutGroupYou know that scroll containers need
layoutScroll