The Mason's First Unset Keystone: On the Shifting Foundation of a Non-Contained Animation
We spend so much time ensuring our layouts are pixel-perfect in their static state, meticulously crafting components that align to the grid. Then we introduce a simple hover animation—a gentle scale transform, a subtle box-shadow—and watch in dismay as the entire structure shudders. Buttons jump, text reflows, and the serene composition we built fractures into a jittery mess. The culprit is rarely the animation itself, but the invisible, uncontained space it creates.
This is the mason's error: placing a keystone without first ensuring the arch is braced. In our context, the keystone is the `transform` property, and the arch is the element's box model. By default, an element undergoing a transformation does not affect the document flow around it. Its original, pre-animation dimensions remain the occupying space. When you scale a button to 1.05 on hover, it visually grows larger, but its siblings don’t budge because the layout engine still reserves the space for the button at scale 1. This is usually a good thing; it prevents the entire page from recalculating layout on every animation frame.
However, the problem emerges with the box-shadow. A shadow, unlike a transform, expands the visual box of the element but, critically, it also expands its layout box. A robust `box-shadow` can add dozens of pixels to an element’s dimensions. When you combine a scale transform that doesn’t affect layout with a shadow that does, you create a visual dissonance. The element grows on two different planes at once, leading to that jarring jump as the surrounding content is forcibly repelled by the newly enlarged shadow box.
The solution is containment. Not the complex Container Queries kind, but the simpler, more foundational act of providing a dedicated, fixed space for the animation to occur within. We must become masons who build a stable plinth for our moving parts. The most straightforward technique is to wrap the animated element and apply a fixed padding to the wrapper that is equal to the maximum expected visual expansion. If your shadow adds 10px and your scale adds another 5%, calculate the maximum possible increase and pad the container accordingly. This creates a ‘stage’ for the animation, isolating its visual effects from the surrounding layout.
Alternatively, you can use `transform: translateZ(0)` to promote the element to its own composite layer, which can sometimes help with performance, but it doesn’t solve the layout shift from properties like `box-shadow`. For that, the wrapper technique is your most reliable tool. It’s a small, deliberate act of foresight—setting the keystone properly from the start. By containing the potential energy of our animations, we ensure our layouts remain stable, not just in stillness, but in the graceful, intended motion that brings them to life.
Notes & further reading
A few pages I came back to while writing this: