Table of Contents
Generate keyframes using JavaScript
Step 1: Calculate the start and end states
Step 2: Generate keyframes
Step 3: Enable CSS animation
Build expandable parts
Performance check
Final consideration
Home Web Front-end CSS Tutorial Performant Expandable Animations: Building Keyframes on the Fly

Performant Expandable Animations: Building Keyframes on the Fly

Apr 08, 2025 am 10:51 AM

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
  };
}
Copy after login

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);
Copy after login

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;
Copy after login

Then we add the steps to the animation string:

 animation = `${step}% {
  transform: scale(${xScale}, ${yScale});
}`;
Copy after login

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});
}`;
Copy after login

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;
}
Copy after login

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
      }
    }
Copy after login

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:

  1. Expand the animation of the wrapper
  2. Reverse expansion animation of content
  3. Animation of fold wrapper
  4. 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});
  }`);
Copy after login

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');
 }
Copy after login

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;
Copy after login

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);
}
Copy after login

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;  
}
Copy after login

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;
}
Copy after login

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!

Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn

Hot AI Tools

Undresser.AI Undress

Undresser.AI Undress

AI-powered app for creating realistic nude photos

AI Clothes Remover

AI Clothes Remover

Online AI tool for removing clothes from photos.

Undress AI Tool

Undress AI Tool

Undress images for free

Clothoff.io

Clothoff.io

AI clothes remover

Video Face Swap

Video Face Swap

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

Hot Tools

Notepad++7.3.1

Notepad++7.3.1

Easy-to-use and free code editor

SublimeText3 Chinese version

SublimeText3 Chinese version

Chinese version, very easy to use

Zend Studio 13.0.1

Zend Studio 13.0.1

Powerful PHP integrated development environment

Dreamweaver CS6

Dreamweaver CS6

Visual web development tools

SublimeText3 Mac version

SublimeText3 Mac version

God-level code editing software (SublimeText3)

Vue 3 Vue 3 Apr 02, 2025 pm 06:32 PM

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.

Can you get valid CSS property values from the browser? Can you get valid CSS property values from the browser? Apr 02, 2025 pm 06:17 PM

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.

A bit on ci/cd A bit on ci/cd Apr 02, 2025 pm 06:21 PM

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

Stacked Cards with Sticky Positioning and a Dash of Sass Stacked Cards with Sticky Positioning and a Dash of Sass Apr 03, 2025 am 10:30 AM

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.

Using Markdown and Localization in the WordPress Block Editor Using Markdown and Localization in the WordPress Block Editor Apr 02, 2025 am 04:27 AM

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

Comparing Browsers for Responsive Design Comparing Browsers for Responsive Design Apr 02, 2025 pm 06:25 PM

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

How to Use CSS Grid for Sticky Headers and Footers How to Use CSS Grid for Sticky Headers and Footers Apr 02, 2025 pm 06:29 PM

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

Google Fonts   Variable Fonts Google Fonts Variable Fonts Apr 09, 2025 am 10:42 AM

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

See all articles