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!

If you've ever had to display an interactive animation during a live talk or a class, then you may know that it's not always easy to interact with your slides

With Astro, we can generate most of our site during our build, but have a small bit of server-side code that can handle search functionality using something like Fuse.js. In this demo, we’ll use Fuse to search through a set of personal “bookmarks” th

I wanted to implement a notification message in one of my projects, similar to what you’d see in Google Docs while a document is saving. In other words, a

Some months ago I was on Hacker News (as one does) and I ran across a (now deleted) article about not using if statements. If you’re new to this idea (like I

Since the early days of science fiction, we have fantasized about machines that talk to us. Today it is commonplace. Even so, the technology for making

I remember when Gutenberg was released into core, because I was at WordCamp US that day. A number of months have gone by now, so I imagine more and more of us

The idea behind most of web applications is to fetch data from the database and present it to the user in the best possible way. When we deal with data there

Let's do a little step-by-step of a situation where you can't quite do what seems to make sense, but you can still get it done with CSS trickery. In this


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

ZendStudio 13.5.1 Mac
Powerful PHP integrated development environment

EditPlus Chinese cracked version
Small size, syntax highlighting, does not support code prompt function

MinGW - Minimalist GNU for Windows
This project is in the process of being migrated to osdn.net/projects/mingw, you can continue to follow us there. MinGW: A native Windows port of the GNU Compiler Collection (GCC), freely distributable import libraries and header files for building native Windows applications; includes extensions to the MSVC runtime to support C99 functionality. All MinGW software can run on 64-bit Windows platforms.

SublimeText3 Chinese version
Chinese version, very easy to use

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