Animating Height Efficiently for Smooth UX and Performance
As web developers, we often face the challenge of creating engaging, animated user interfaces while maintaining optimal performance across devices. One of the most common animations is expanding and collapsing content by animating an element‘s height. However, the way height is typically animated can lead to significant performance issues, especially on lower-end devices.
In this article, we‘ll explore the challenges with animating height directly in CSS, why efficient animation is crucial for user experience and business metrics, and dive into techniques to animate height smoothly using creative workarounds. We‘ll also look at implementing these efficient animations in React using reusable components.
The Problem with Animating Height in CSS
When we animate the height property of an element, it might seem like a straightforward way to expand or collapse content. However, under the hood, the browser has to perform expensive calculations and operations.
Changing the height triggers the browser to recalculate the layout of the entire page and then repaint it on every frame of the animation. These layout and paint operations are costly in terms of CPU resources and can quickly lead to choppy, stuttering animations, especially on mobile devices.
"Animating properties is not free, and some properties are cheaper to animate than others. For example, animating the width and height of an element changes its geometry and may cause other elements on the page to move or change size. This process is called layout (or reflow), and can be expensive if your page has a lot of elements. Whenever layout is triggered, the page or part of it will normally need to be painted, which is typically even more expensive than the layout operation itself."
– Google Web Fundamentals
To see the performance cost of various CSS properties, check out CSS Triggers which breaks down what operations each property triggers.
Why Performance Matters
You might be tempted to overlook performance in favor of shipping features faster. However, the reality is that performance has a direct impact on user experience, engagement, and ultimately, your bottom line.
Numerous studies by tech giants like Amazon, Google, and Walmart have shown the correlation between page load times, user behavior, and key business metrics:
- Amazon found that a 100ms increase in load time reduced sales by 1%. (Source: GigaSpaces)
- Google discovered that a 0.5 second delay in search results caused a 20% drop in traffic. (Source: Think With Google)
- Walmart saw a 2% increase in conversions for every 1 second improvement in load time. (Source: Web Performance Today)
Beyond revenue impact, we as developers have a responsibility to ensure the web remains accessible to everyone, not just those with high-end devices and fast connections.
"The cost of parsing and compiling a JavaScript bundle can be significant, especially on low-end mobile devices. Compared to the main thread time on a high-end device, Parse times can be up to 36x longer on low-end mobile."
– Addy Osmani, The Cost of JavaScript in 2018
To give all users a good experience and avoid widening the gap in internet access, we must be diligent in optimizing performance. This motivated me to find alternative, more efficient ways to animate expanding/collapsing content without giving up the smooth, polished feel.
Smoothly Animating Height with Transforms
The key to performant height animation is avoiding animating the height property directly. Instead, we need to leverage other CSS properties that are less costly.
The transform
property, specifically scale
is a good candidate. Transforms don‘t trigger expensive layout operations and are often handled on the GPU. The downside is that they don‘t actually affect the element‘s dimensions.
Here‘s the clever workaround I came up with:
- Measure the initial height of the element and set an explicit height on the outer container to prevent content changes from expanding it.
- Add a
<div>
inside the container with an absolute position to fill the entire width and height. This acts as the background. - Place the actual content inside another nested
<div>
. This inner container‘s height will change based on the content. - When content changes (e.g. expanding an accordion), take the new height of the inner container and calculate how much the background
<div>
needs to scale vertically to create the illusion of the outer container changing height. - Apply a
transform: scaleY()
to the background<div>
using the calculated scale value.
Here‘s a simplified code example:
<div class="outer">
<div class="background"></div>
<div class="inner">
<!-- content goes here -->
</div>
</div>
const outer = document.querySelector(‘.outer‘);
const inner = document.querySelector(‘.inner‘);
const background = document.querySelector(‘.background‘);
// Get initial height
const initHeight = inner.clientHeight;
outer.style.height = `${initHeight}px`;
// Expand content
inner.innerHTML += `...`;
// Calculate scale based on new height
const newHeight = inner.clientHeight;
const scale = newHeight / initHeight;
// Apply vertical scale transform
background.style.transform = `scaleY(${scale})`;
With this technique, the background scales to match the content height creating a smooth expanding/collapsing animation without the performance hit of reflow or repaint. The only caveat is positioning of subsequent elements isn‘t automatically adjusted since there‘s no actual change to the document flow.
To move surrounding elements in sync with the simulated height change, we need to apply a transform: translateY()
on them equal to the difference between the expanded and initial height. This completes the illusion.
const nextElement = outer.nextElementSibling;
const translateY = newHeight - initHeight;
nextElement.style.transform = `translateY(${translateY}px)`;
You can view a live demo of this technique here to see it in action.
Keep in mind, when moving elements with translateY
, you may need to add extra vertical padding to the bottom of the page equal to the translation amount to prevent cutting off any content.
Animating Height in React Components
I first implemented this efficient height animation technique in a resume-building side project using React. It worked great so I decided to abstract it into a reusable React component library.
The result was react-anim-kit – a small set of components that make it easy to add performant, smooth height animations to your React app without having to work out the low-level details yourself.
The library provides two key components:
<AnimateHeight>
– Wraps content that expands/collapses and handles efficient animation when triggered by a state change.<AnimateHeightContainer>
– Wraps a section where one or more<AnimateHeight>
components are rendered. Automatically adjusts position of sibling components based on currently expanded item.
Here‘s a basic example of how they can be used:
import { AnimateHeight, AnimateHeightContainer } from ‘react-anim-kit‘;
function MyComponent() {
const [isOpen, setIsOpen] = React.useState(false);
return (
<AnimateHeightContainer>
<button onClick={() => setIsOpen(!isOpen)}>
Toggle Content
</button>
<AnimateHeight shouldChange={isOpen}>
{isOpen && (
<div>
{/* content to expand/collapse */}
</div>
)}
</AnimateHeight>
<p>I will be transitioned when the above content expands!</p>
</AnimateHeightContainer>
);
}
The <AnimateHeight>
component takes a shouldChange
prop which should be set to a state variable that toggles true
/false
to trigger the animation.
Any content that should be revealed when expanded is placed inside <AnimateHeight>
and conditionally rendered based on the same state variable.
The <AnimateHeightContainer>
component automatically handles translating subsequent sibling elements of an <AnimateHeight>
whenever an expansion/collapse occurs within it.
You can also use the animateHeightId
prop to manually specify the order in which sibling elements should be translated for more complex layouts. Elements with a lower ID will not be moved when an element with a higher ID expands:
<AnimateHeightContainer>
<div animateHeightId={1}>
<AnimateHeight animateHeightId={1}>
{/* ... */}
</AnimateHeight>
</div>
{/* Below elements will move when above animates */}
<div animateHeightId={2}>...</div>
<div animateHeightId={3}>...</div>
</AnimateHeightContainer>
This is just a high-level overview but you can read the full documentation to learn more about the component props and advanced usage.
I‘ve also put together a few demos on CodeSandbox to show what you can build with react-anim-kit:
Using a component library Abstract the underlying implementation details and makes efficient height animation a breeze to add To your React apps. Of course, you can always build Your own abstraction on top of the fundamental techniques we covered earlier If you want more flexibility and control.
Key Takeaways
At the end of the day, The goal is to find ways To craft performant, beautiful interfaces That our users love without compromising On the experience for those on constrained devices.
Animating height, and really any property, with CSS should be approached thoughtfully with Performance in mind. Don‘t Just reach for The simplest, most obvious implementation Without considering the implications.
By leveraging the techniques in this article, You can achieve smooth, Performant height animations Without giving up The engaging Expanding/collapsing effect:
- Avoid animating height directly to prevent costly reflow and repaint operations, especially on mobile devices
- Use transform: scale() to transition a background element and create the illusion of height changing
- Complement the scale animation with translateY() on subsequent elements to maintain the flow appearance
- Consider abstracting efficient animation techniques into reusable components for your framework of choice
Most importantly, don‘t forget the reason behind all of this: Building a web that is accessible to all and providing an excellent user experience to everyone, regardless of their device or network.
To quote World Wide Web creator Tim Berners-Lee:
"The power of the Web is in its universality. Access by everyone regardless of disability is an essential aspect."
As web developers, we have a responsibility to uphold that vision. Performance is a core part of accessibility and something we should always strive to prioritize and improve.
I hope this deep dive into efficient height animation has equipped you with the knowledge and tools to build fast, inclusive interfaces. Now go forth and animate (responsibly)!