Performant Expandable Animations: Building Keyframes on the Fly
CSS animation technology is becoming increasingly mature, providing developers with more powerful tools. CSS animation in particular has become the basis for solving most animation use cases. However, some animations require more fine-grained processing.
As we all know, animations should be run in the synthesis layer (I will not repeat it here, if you are interested, please refer to the relevant literature). This means that the animation's transformation or opacity attributes do not touch the publishing layout or draw layer. Attributes such as animation height and width will trigger these layers, forcing the browser to recalculate the style, which needs to be avoided.
In addition, even if you want to implement real 60 FPS animations, you may also need to use JavaScript, such as using FLIP technology to achieve smoother animations!
However, the problem with using transform properties for expandable animation is that the scaling function is not exactly the same as the animation height/width properties. It will have a skew effect on the content, as all elements will be stretched (scaled up) or squeezed (scaled down).
So my common solution (probably still, the reason will be explained in detail later) is Method 3 in Brandon Smith article. This approach still makes a transition to height, but uses JavaScript to calculate the content size and forces the transition using requestAnimationFrame. In OutSystems, we actually use this method to build an animation for OutSystems UI accordion mode.
Generate keyframes using JavaScript
Recently, I stumbled upon another great article by Paul Lewis, detailing a new solution for unfolding and folding animations, which prompted me to write this article and promote this technology.
In his words, the main idea is to generate dynamic keyframes and gradually...
[…] From 0 to 100, and calculate the scaling value required for the element and its content. These values can then be simplified to a string and injected into the page as a style element.
There are three main steps to achieve this.
Step 1: Calculate the start and end states
We need to calculate the correct scaling value for both states. This means we use getBoundingClientRect()
on the element that will be proxyed as the starting state and divide it by the value of the end state. It should look like:
function calculateStartScale () { const start= startElement.getBoundingClientRect(); const end= endElement.getBoundingClientRect(); return { x: start.width / end.width, y: start.height / end.height }; }
Step 2: Generate keyframes
Now we need to run a for loop using the required number of frames as length. (To ensure smooth animation, it should not be less than 60 frames.) Then, in each iteration, we use the easing function to calculate the correct easing value:
function ease (v, pow=4) { return 1 - Math.pow(1 - v, pow); } let easedStep = ease(i/frame);
Using this value, we will use the following mathematical formula to get the scaling of the element in the current step:
const xScale = x (1 - x) * easedStep; const yScale = y (1 - y) * easedStep;
Then we add the steps to the animation string:
animation = `${step}% { transform: scale(${xScale}, ${yScale}); }`;
To avoid the content being stretched/tilted we should animate it inversely, using the reverse value:
const invXScale = 1 / xScale; const invYScale = 1 / yScale; inverseAnimation = `${step}% { transform: scale(${invXScale}, ${invYScale}); }`;
Finally, we can return the completed animations, or inject them directly into the newly created style tag.
Step 3: Enable CSS animation
In terms of CSS, we need to enable animation on the correct elements:
.element--expanded { animation-name: animation; animation-duration: 300ms; animation-timing-function: step-end; } .element-contents--expanded { animation-name: inverseAnimation; animation-duration: 300ms; animation-timing-function: step-end; }
You can view the menu examples in the Paul Lewis article (contributed by Chris) on Codepen.
Build expandable parts
Having mastered these basic concepts, I wanted to check if this technique can be applied to different use cases, such as expandable parts.
In this case, we only need to animate the height, especially in the function that calculates the scaling. We get the Y value from the section title as the collapsed state and get the entire section to represent the expanded state:
_calculateScales () { var collapsed = this._sectionItemTitle.getBoundingClientRect(); var expanded = this._section.getBoundingClientRect(); // create css variable with collapsed height, to apply on the wrapper this._sectionWrapper.style.setProperty('--title-height', collapsed.height 'px'); this._collapsed = { y: collapsed.height / expanded.height } }
Since we want the expanded part to have absolute positioning (to avoid it taking up space in the collapsed state), we set a CSS variable for it using the collapsed height and apply it to the wrapper. This will be the only element with relative positioning.
Next is the function to create the keyframe: _createEaseAnimations()
. This is not much different from what is explained above. For this use case, we actually need to create four animations:
- Expand the animation of the wrapper
- Reverse expansion animation of content
- Animation of fold wrapper
- Reverse folding animation of content
We follow the same approach as before, running a for loop of 60 length (to get a smooth 60 FPS animation) and creating the keyframe percentage based on the easing step. Then we push it to the final animation string:
outerAnimation.push(` ${percentage}% { transform: scaleY(${yScale}); }`); innerAnimation.push(` ${percentage}% { transform: scaleY(${invScaleY}); }`);
We first create a style tag to save the finished animation. Since this is built as a constructor, to be able to easily add multiple patterns, we want all of these generated animations to be in the same stylesheet. So first, we verify that the element exists. If it does not exist, we create it and add a meaningful class name. Otherwise, you end up getting a stylesheet for each expandable section, which is not ideal.
var sectionEase = document.querySelector('.section-animations'); if (!sectionEase) { sectionEase = document.createElement('style'); sectionEase.classList.add('section-animations'); }
Speaking of this, you might already be thinking, "Well, if we have multiple expandable parts, do they still use the animation of the same name, and their contents may have the wrong value?"
You are absolutely right! So to prevent this, we also generate dynamic animation names. Very cool, right?
When doing querySelectorAll('.section')
query to add a unique element to the name, we use the index passed to the constructor:
var sectionExpandAnimationName = "sectionExpandAnimation" index; var sectionExpandContentsAnimationName = "sectionExpandContentsAnimation" index;
We then use this name to set the CSS variable in the currently expandable section. Since this variable is only within this range, we just need to set the animation as a new variable in CSS and each pattern will get its respective animation-name
value.
.section.is--expanded { animation-name: var(--sectionExpandAnimation); } .is--expanded .section-item { animation-name: var(--sectionExpandContentsAnimation); } .section.is--collapsed { animation-name: var(--sectionCollapseAnimation); } .is--collapsed .section-item { animation-name: var(--sectionCollapseContentsAnimation); }
The rest of the script is related to adding event listeners, functions that toggle collapse/expand states, and some helper function improvements.
About HTML and CSS: Additional work is required to make the expandable function work properly. We need an extra wrapper as a relative element that does not perform animation. Expandable child elements have absolute positions so they do not take up space when collapsed.
Remember that since we need to do reverse animation, we make it scale full size to avoid tilting effects in the content.
.section-item-wrapper { min-height: var(--title-height); position: relative; } .section { animation-duration: 300ms; animation-timing-function: step-end; contains: content; left: 0; position: absolute; top: 0; transform-origin: top left; will-change: transform; } .section-item { animation-duration: 300ms; animation-timing-function: step-end; contains: content; transform-origin: top left; will-change: transform; }
I want to emphasize the importance of animation-timing-function
property. It should be set to linear
or step-end
to avoid easing between each keyframe.
The will-change
property—as you probably know—will enable GPU acceleration for transform animations for a smoother experience. Using the contains
property, with a value of contents
, will help the browser process elements independently of the rest of the DOM tree, limiting the area where it recalculates the layout, style, draw, and size attributes.
We use visibility
and opacity
to hide content and prevent screen readers from accessing it when collapsed.
.section-item-content { opacity: 1; transition: opacity 500ms ease; } .is--collapsed .section-item-content { opacity: 0; visibility: hidden; }
Finally, we have the part that can be expanded! Here is the complete code and demonstration for you to view:
Performance check
Whenever we deal with animations, performance should be kept in mind. So let's use developer tools to check if all this work is worth it in terms of performance. Using the Performance tab (I'm using Chrome DevTools), we can analyze FPS and CPU usage during animation.
The result is very good!
Checking the value in more detail using the FPS measurement tool, we can see that it always reaches the mark of 60 FPS, even for abuse.
Final consideration
So, what is the final conclusion? Does this replace all other methods? Is this the "Holy Grail" solution?
In my opinion, no.
But...this doesn't matter! It's just another solution on the list. And, like any other approach, it should be analyzed whether it is the best way to target a use case.
This technology has its advantages. As Paul Lewis said, this does require a lot of work to prepare. However, on the other hand, we only need to do it once when the page is loading. During the interaction, we simply switch classes (in some cases, for accessibility, and also switch properties).
However, this puts some limitations on the element's UI. As you can see in expandable partial elements, reverse scaling makes it more reliable for absolute and off-canvas elements, such as floating actions or menus. Since it uses overflow: hidden
, it is difficult to style the border.
Nevertheless, I think this approach has great potential. Please tell me what you think!
The above is the detailed content of Performant Expandable Animations: Building Keyframes on the Fly. For more information, please follow other related articles on the PHP Chinese website!

Hot AI Tools

Undresser.AI Undress
AI-powered app for creating realistic nude photos

AI Clothes Remover
Online AI tool for removing clothes from photos.

Undress AI Tool
Undress images for free

Clothoff.io
AI clothes remover

Video Face Swap
Swap faces in any video effortlessly with our completely free AI face swap tool!

Hot Article

Hot Tools

Notepad++7.3.1
Easy-to-use and free code editor

SublimeText3 Chinese version
Chinese version, very easy to use

Zend Studio 13.0.1
Powerful PHP integrated development environment

Dreamweaver CS6
Visual web development tools

SublimeText3 Mac version
God-level code editing software (SublimeText3)

Hot Topics

It's out! Congrats to the Vue team for getting it done, I know it was a massive effort and a long time coming. All new docs, as well.

I had someone write in with this very legit question. Lea just blogged about how you can get valid CSS properties themselves from the browser. That's like this.

I'd say "website" fits better than "mobile app" but I like this framing from Max Lynch:

The other day, I spotted this particularly lovely bit from Corey Ginnivan’s website where a collection of cards stack on top of one another as you scroll.

If we need to show documentation to the user directly in the WordPress editor, what is the best way to do it?

There are a number of these desktop apps where the goal is showing your site at different dimensions all at the same time. So you can, for example, be writing

CSS Grid is a collection of properties designed to make layout easier than it’s ever been. Like anything, there's a bit of a learning curve, but Grid is

I see Google Fonts rolled out a new design (Tweet). Compared to the last big redesign, this feels much more iterative. I can barely tell the difference
