Matt Perry
is writing
code.

Layout projection: A method for animating browser layouts at 60fps

Downsides

Layout projection is powerful and can be applied in a variety of ways. But it does have a couple of serious drawbacks.

Here, I've tried to stick to problems that you are very likely to encounter as someone creating a library around the technique based on the previous section, rather than things that are simply bugs in Framer Motion's implementation.

So at the risk of sounding like a job interview dickhead, the first:

It’s too good

Bear with me!

An inherent output of layout projection is its scale correction. It's extremely robust, but the most robust (and simplest) implementation is ensuring all ProjectionNodes are always "on", responding to any changes in parent scale.

This causes problems. Take the app icon example from before. To create it, I wanted to reuse the first demo of this post, the App Store-style demo. So I nested it neatly in my faux home screen component, which lead to this effect:

Notice how the app icon performs the correct animation, as before, but the app itself appears to just fade in and out, static on-screen.

What's happening here is that in the original App Store-style demo, all the elements had the layout prop, so when we click between its item list view to the full-screen view these elements smoothly animate between layouts.

But now, these same elements are nested inside a shared element crossfade transition. It's a different animation, but these children are still performing layout correction to perfectly counteract the scaling effect!

Take the draggable component from before, now with an actively layout-projecting child:

The child completely counteracts the movement of its parent!

In a way this stuff is kind of cool, and I'm sure in the future it will have some application. But it certainly isn't what we want in these instances.

This is currently marked as a bug in Framer Motion, and I'm exploring a few ways to fix it.

One would simply to be more aggressive in disabling layout projection, or perhaps switching to a scale-only mode, when a whole tree branch has finished a layout animation.

Another way, that I'm currently leaning towards, would be to derive each element's viewport bounding box relative to its parent projecting element. This would enhance layout animation orchestration and fix both of the examples above, at the expense of a more complex implementation.

Scroll

The eagle-eyed amongst you might have noticed that we've been using a viewport-relative box to project into, but we haven't updated that box when the page scrolls.

This isn't a problem in itself, as we also don't update the measured layout with page scroll either. So the measured layout and viewport box, and thus the calculated projection transform, all remain relatively correct throughout the scroll.

There is one instance where this does become a problem. When crossfading between two different DOM trees, where one of those trees responds to scroll and the other doesn't (like a position: fixed element), the common viewport box reference is broken.

Theoretically it'd be possible to incorporate scroll offset into the projection calculations of the fixed element, but scroll-bound animations often suffer jitter as many browsers prioritize updating the screen with scroll over requestAnimationFrame updates.

Measuring window.scrollY also unfortunately triggers layout so it isn't performant to get this offset to begin with.

Until there's a way around these limitations, blocking page scroll during these transitions is probably the best way to fix it.

The main thread

Most parts of layout projection are currently bound to the main thread.

The viewport box data structure has to be animated by a JavaScript animation library as we can't get raw values out of the Web Animation API or CSS transitions. Or we're generating it from pointer events. In addition, the projection calculations for n-deep trees need to run synchronously down the tree.

This is all surprisingly lightweight - profiling extreme implementations in Framer prototypes show that rendering will be the bottleneck long before layout projection.

But what it means is that if the page is performing heavy workloads you might notice that in your layout animations. One of the best applications is shared element transitions, a time we're more likely to be loading new data or rendering new content.

As always, you should complete big tasks in the 100ms perceptual window before triggering animations.

Maybe in the future it'll be possible to write something off-thread with the Animation Worklet or WebAssembly. Neither of these are ready for even basic animation tasks, so it might be a while yet.

Until then, it is necessary to use a JavaScript animation library like Popmotion.

Conclusion

Despite these downsides, even the early implementation of layout projection found in Framer Motion can be used to tackle tasks that were once insurmountably difficult.

Layout animations and shared element transitions are two areas in which the web experience always falls behind the app equivalent.

It's my hope that after reading this post, people will feel inspired to create more libraries incorporating layout projection to help developers across the front end ecosystem close down this quality gap.

Perhaps, as layout projection matures, and the kinks of implementation are smoothed out, the problem space better understood, we can start to work towards a native browser API that would alleviate the dependency on external libraries.

Photo of Matt Perry
Matt Perry is a developer and photographer currently living in Amsterdam.

Twitter | Instagram