Matt Perry
is writing
code.

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

The problem of layout animation

Animating browser-computed layout isn't a new problem, it's been difficult for decades.

It's easy to imagine why. CSS offers a number of layout systems; grid, flexbox, floats, and more. The final calculated layout of any web page is an interaction between the rules placed on these systems and the HTML hierarchy they're applied to.

Owing in part to this flexibility/complexity, these layout calculations are often too expensive to run at the 60fps required for smooth animations.

Furthermore, these layout systems are systems of constraints, the output from which is often the result of discrete rules. What does an interim state between flex-start and flex-end look like? Or between display: grid and display: flex? Or animating from three columns to one?

No layout is calculated in isolation. The computed size and position of one element affects those in and around it. Therefore, it's just as odd to want to animate these obviously problematic discrete systems as it is for more common wishlist items, like height: auto.

In fact, it is already possible to animate some explicit layout rules like setting width and height to an explicit value.

width: 200px;
transition: width 1s ease-out;

But let's see this in action.

I must not animate layout. I must not animate layout. I must not animate layout. I must not animate layout. I must not animate layout.

As a simple page, this animation should actually perform fairly well on modern devices, but there's a few things to notice.

The first is, as part of the layout calculations, width is being rounded to full pixels. Especially at low velocities, this can make the animation feel janky.

The second, in my opinion more important point, is that the line-wrapping is (understandably) a system of binary break points. A word either wraps or it doesn't, and this changes throughout the animation, leading to this constant re-wrapping. It looks terrible.

So even when animating layout is performant, even when we are animating seemingly straightforward values, I don't believe that the resultant visual output is something we should bother to pursue anyway.

Instead, what we want is a system that lives apart from the nexus of CSS layout and its discrete rules. One that lets the browser calculate the layout and then animates between the raw image output of each.

Not only would this kind of system look and perform better, it would work with any calculated layout system and work out-of-the-box with arbitrary systems created via the (allegedly) upcoming Houdini Layout API.

Attempts have been made at this kind of system, not only in browsers but also in other calculated layout systems. Layout projection has its roots firmly in these previous systems, so let's take a brief look.

Prior art

Animating layout isn't a browser-specific problem. Any constraints-based layout system will suffer some or all of the above problems.

Microsoft has a whole series of patents around layout animations, including one from 2009 based on its Windows Presentation Foundation (WPF) UI framework. It describes a multi-step process that every layout animation method broadly adheres to:

  1. Snapshot the current layout
  2. Apply the new layout and snapshot
  3. Use the delta between snapshots to put any moved elements back in their previous size and position
  4. Animate the deltas to zero

I'm not familiar with WPF but it's interesting from the paper that it seems to suffer many of the same limitations as the DOM and CSS.

For instance, it mentions that animating one element's layout could affect the layout of others, just like the DOM. The proposed solution is to pause the broadcasting of layout updates for the duration of the animation, which isn't a privilege we have in the browser but an interesting implementation detail all the same.

When discussing the poor performance of animating layout systems, it proposes wrapping the animating elements in a non-layout managed layer and taking direct control of size and positioning.

The browser analogue to this approach might be wrapping the animating element with another absolutely-positioned container. The new contain rule could isolate layout changes in this wrapper from surrounding elements.

.isolate {
contain: size layout;
}

But animating such an element via top or width would still trigger some layout, so this wouldn't yield scalable performance benefits (or any if we consider full-hierarchy shared element transitions).

Management of this extra wrapper is also fraught with its own complexity and performance implications.

Luckily, CSS does provide a rule that can change the size and position of an element without triggering layout calculations: transform. Which brings us to the current go-to method of layout animations on the web, FLIP.

FLIP

Created by Google's Paul Lewis, FLIP is a method for performing layout animations using transform.

FLIP is an acronym for First, Last, Invert, Play, the four steps of a layout animation as described in the Microsoft patent.

The key difference with FLIP is the "I", Invert. It means calculating a transform that will make an element, from its new layout, look like it's in the size and position of its old one:

const x = prev.left - current.left;
const y = prev.top - current.top;
const scaleX = prev.width / current.width;
const scaleY = prev.height / current.height;
element.style.transform = `translate(${x}px, ${y}px) scale(${scaleX}, ${scaleY})`;

Once this inverse delta has been calculated, and the element is visually back in its old layout, performing the animation is a matter of animating x/y to 0 and scale to 1. With CSS, we can do this by simply removing the transform:

requestAnimationFrame(() => {
element.style.transition = "transform 300ms ease-out";
element.style.transform = "";
});

FLIP is great because it's easy to understand, achievable using only CSS animations, and about as performant as a browser animation can get. But it does come with its own set of drawbacks.

Animating position using a transform is straightforward:

But when we start animating an element's size via the scale transform, we introduce scale distortion. Notice, in this next example, that both the parent's border-radius and white child element both change size throughout the animation, even though they're the same size in both layouts they're animating between:

This isn't only a visual distortion: As the parent animates its size via scale, it's also scaling the coordinate space for all of its children.

Practically, this means that if a parent has a scale of 0.5, and its child has a translateX of 100px, the child will only appear to move 50px on screen:

scaleX: 1
scaleX: 0.5

Animating such a child's position with any kind of control over its exact position or easing curve is simply impossible with CSS if that parent is constantly animating this scale value.

Because of these drawbacks, the complexity curve from the simple layout animation to more advance use-cases like a shared element transition remains insurmountably steep.

Layout projection keeps the same principle of using transform to change the layout but as we'll see, fixes these problems and opens up further possibilities.

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

Twitter | Instagram