Intro

In this article we’ll dive into the Transition animation. Yes, the CSS3 Transition animation. Isn’t it easy, you might ask, what is there to say?

Indeed, the Transition animation is very easy to use. Filter elements by adding transition-delay, transition-duration, transition-property, and transition-timing-function attributes. It’s even easier to use the transition property as a shorthand:

transition: <property> <duration> <timing-function> <delay>;

// transition-delayThe default is0
// transition-propertyThe default is all //transition-timing-functionDefaults to easetransition: 0.3 s;
Copy the code

Since it costs almost nothing to use transition animation, AND I haven’t studied it in depth, I recently found that SOME knowledge is not fully understood after reviewing the source code and MDN documents. Therefore, THIS article is written in the hope that it will help readers to have a deeper understanding of Transition animation.

In order to keep the cost of reading comprehension to a minimum, this article will be written a little bit longer, and most of the examples will be illustrated with pictures – “Picture alert!”

What is the Transition?

To put it simply, it is a transition animation. Usually, changing the style of DOM nodes is immediately updated on the page, such as changing width and height, transparency, background color and so on.

For example, when the mouse moves over the button, in order to highlight the interaction of the button, it will modify its style in hover, so that users can notice it. Without the transition animation, the user will feel stiff and stiff.

.button{/ /...background-color: #00a8ff;
}

.button:hover {
  background-color: #fbc531;
  transform: scale(1.2);
}
Copy the code

And then you add a line of transition, and it’s going to be a little smoother.

.button{/ /...transition: 1s;
}
// ...
Copy the code

In this example, we’ve changed background-color and transform, and the browser will automatically animate the transition property over time, from the old value to the new value.

Different from Animation, Transition focuses on showing a Transition effect from the beginning to the end. Animation does not need to change and can play ▶️ in a loop.

Note that not all property changes have a transition effect

  1. Some CSS properties support only enumerated values, black or white, with no intermediate states, for examplevisibility: visible;Be modified intovisibility: hidden;There will be no animation because there are no visible and invisible in-between states. In the browser, the element mutates to hidden as soon as duration arrives.
    .button:hover {
      //...
      visibility: hidden;
    }
    Copy the code

  2. Some attributes, while computable, are inherently non-transitive, for exampletransition-delay.transition-durationWith immediate effect, it’s worth adding here becausetransition-*Attribute is immediate. If this line of code is hover, then the effect will be hover with animation, and remove without animation.
  3. Even transitive attribute changes may lose their transitive effect because they cannot compute intermediate states. For example,box-shadowThe transition property supports animation, but if the transition is animated from”outsetSwitch to the”inset, and it is alsomutation.
    .button{/ /...box-shadow: 0 0 0 1px rgb(0 0 0 / 15%);
      transition: 1s;
    }
    
    .button:hover{/ /...box-shadow: inset 0 0 0 10px rgb(0 0 0 / 15%);
    }
    Copy the code



    In terms of performance, box-shadow changes hover immediately.

  4. Transitions also do not take effect if an attribute value is continuously computable, but becomes a hash enumeration before and after the change. For example, fromheight: 100px= >height: autoThere will be no animation.

Now that we’ve reviewed the basic use of Transition, let’s look at a problem we might encounter in a real development scenario.

Why didn’t the Transition animation work?

Scenario: Suppose we now receive an animation request for a custom pull-down selector. The designer gives us the following image:

This is a very common appearing-and-disappearing animation, which occurs in many component libraries. The Popup is rendered on the page when the trigger is clicked, and the Popup requires both a fade and a drop animation; After expanding, click the button again, Popup needs to fade out and slide up.

Usually used when did not pay too much attention to its implementation, might as well now let us hands-on experiment.

Ignore the popup content for a moment and use a placeholder div to simulate the HTML structure is simple.

<div class="wrapper">
    <div id="button"></div>
    <div id="popup"></div>
</div>
Copy the code

Make popUp show/hide when the button is clicked, and then switch the.active class name for PopUp.

const btn = document.querySelector("#button");
const popup = document.querySelector("#popup");

if(! popup.classList.contains("active")) {
    popup.style.display = "block";
    popup.classList.add("active");
} else {
    popup.style.display = "none";
    popup.classList.remove("active");
}
Copy the code

Write CSS styles with transparency set to 0 and offset up when not active and 1 when active.

#popup {
  display: none;
  opacity: 0;
  transform: translateY(-8px);
  transition: 1s;

  &.active {
    opacity: 1;
    transform: translateY(0%); }}Copy the code

The full code is here, and it looks like there’s nothing wrong with the code. Popup should have an animated transition when clicking the button to switch. In practice, however:

Hard transitions have no effect at all. Why? While transition is already set, also the values on opacity and translateY are computable and transitive, so the browser does not recognize the changes.

Before checking the documentation, we first try using setTimeout.

Scheme 1: setTimeout ten thousand essential oils

Modify the JS code:

btn.addEventListener("click".() = > {
  if(! popup.classList.contains("active")) {
    popup.style.display = "block";
    setTimeout(() = > {
      popup.classList.add("active");
    }, 0);
  } else {
    popup.classList.remove("active");
    setTimeout(() = > {
      popup.style.display = "none";
    }, 600); }});Copy the code

You can see that when you add setTimeout, the Transition animation takes effect.

The setTimeout 600ms corresponds to the transition set in the CSS: 0.6s, that is, display is set to None after the animation is complete.

The main point of confusion is why setTimeout is needed for display? What does setTimeout 0 do here? Review the specification documentation with questions in mind.

Find the following in the Starting of Transitions section of the specification documentation:

When a style change event occurs, implementations must start transitions based on the computed values that changed in that event. If an element is not in the document during that style change event or was not in the document during the previous style change event, then transitions are not started for that element in that style change event.

When a style change event occurs, the implementation (browser) must perform a transition animation based on the changed properties. However, if the element is not in the document when the style change event occurs or during the last style change event, the transition animation is not started for that element.

Combined with the RenderTree construction process in the browser, we can clearly locate the problem: display: None DOM elements do not appear in RenderTree (style.display=’block’ is not synchronized and will not be updated to RenderTree until next rendering). The conditions for Starting of Transitions are not met.

So setTimeout 0 is used to call up a MacroTask, and by the time EventLoop executes its callback, the browser has completed a rendering, plus the.active class name, which is sufficient to execute the transition animation.

Optimization plan 2: Accurate card requestAnimationFrame

Since the purpose is to have elements appear in the RenderTree first, relative to rendering, it is easy to think of replacing setTimeout with requestAnimationFrame, which would be more precise, since requestAnimation execution timing depends on rendering.

if(! popup.classList.contains("active")) {
    popup.style.display = "block";

    requestAnimationFrame(() = > {
        popup.classList.add("active");
    });
}
Copy the code

A small sideband: While searching for information, I learned that the requestAnimationFrame specification requires its callback function to be executed before the Style/Layout phase, which Chrome and Firefox originally followed. Safari and Edge are executed later. Chrome and Firefox have also changed to execute later, looking at the previous document and saying that you need to nest two layers of requestAnimationFrame, which is no longer necessary. Is requestAnimationFrame called at the right point?

Solution 3: Force Reflow

In the specification document, note the following sentence:

Implementations typically have a style change event to correspond with their desired screen refresh rate, and when up-to-date computed style or layout information is needed for a script API that depends on it.

This means that browsers generally generate style changes in two other situations, one is to meet the screen refresh rate (requestAnimationFrame). The second is when the JS script needs to get the latest style layout information.

In the JS code, some of the API is called, the browser will simultaneously calculate the style and layout, frequently called these apis (offset * / client/scroll * / * / getBoundingClientRect… And so on) are often performance bottlenecks.

In this scene, however, strange chemistry can occur:

if(! popup.classList.contains("active")) {
  popup.style.display = "block";
  popup.scrollWidth;
  popup.classList.add("active");
}
Copy the code

Notice that we just read the scrollWidth between display and Add class, without even assigning, and the transition animation comes to life.

The reason is that scrollWidth forced synchronization triggers a rearrange redraw, and the next line of code, popup’s display property has been updated to the Render Tree.

Optimization plan 4: Tell me onTransitionEnd when you’re done

Vue, Bootstrap, and react-transition-group all use the force Reflow method. Antd uses the CSS-Animte library by setting setTimeout.

[Vanish] The animation is not elegant enough. We just killed setTimeout 600 and let the element disappear at the end of the animation. Such coding reusability is poor, modify the animation time also have to change two places (JS + CSS), is there a more elegant implementation?

popup.classList.remove("active");
setTimeout(() = > {
    popup.style.display = "none";
}, 600);
Copy the code

Transition Events are also mentioned in the documentation, including transitionRun, transitionStart, transitionEnd, and transitionCancel. Code optimization can be done here with transitionEnd.

if(! popup.classList.contains("active")) {
    popup.style.display = "block";
    popup.scrollWidth;
    popup.classList.add("active");
} else {
    popup.classList.remove("active");
    popup.addEventListener('transitionend'.() = > {
        popup.style.display = "none";
    }, { once: true})}Copy the code

Note that Transition Events also have the ability to bubble and capture. If you have nested transitions, watch out for event.target.

So far we have used native JS to complete a appear and disappear animation implementation, the complete code is here. At the end of the article, we develop a minimal implementation of the React Transition single element animation using vue-Transition.

Implement a React Transition component that mimkes the V-Transition

According to the animation process, it is divided into several processes:

  • The Enter stage renders the DOM node and initializes the initial state of the animation (add*-enterThe name of the class)
  • The Enter-active stage performs the Transition animation (add*-enter-activeThe name of the class)
  • After the transition is complete, enter the normal display phase (remove*-enter-activeThe name of the class)

Enter -to and leave-to are not used temporarily, and the leave phase is basically the same as enter.

Look directly at the code:


export const CSSTransition = (props: Props) = > {
  const { children, name, active } = props;
  const nodeRef = useRef<HTMLElement | null> (null);
  const [renderDOM, setRenderDOM] = useState(active);

  useEffect(() = > {
    requestAnimationFrame(() = > {
      if (active) {
        setRenderDOM(true); nodeRef.current? .classList.add(`${name}-enter`);
        // eslint-disable-next-line @typescript-eslint/no-unused-expressionsnodeRef.current? .scrollWidth; nodeRef.current? .classList.remove(`${name}-enter`); nodeRef.current? .classList.add(`${name}-enter-active`); nodeRef.current? .addEventListener("transitionend".(event) = > {
          if(event.target === nodeRef.current) { nodeRef.current? .classList.remove(`${name}-enter-active`); }}); }else{ nodeRef.current? .classList.add(`${name}-leave`);
        // eslint-disable-next-line @typescript-eslint/no-unused-expressionsnodeRef.current? .scrollWidth; nodeRef.current? .classList.remove(`${name}-leave`); nodeRef.current? .classList.add(`${name}-leave-active`); nodeRef.current? .addEventListener("transitionend".(event) = > {
          if(event.target === nodeRef.current) { nodeRef.current? .classList.remove(`${name}-leave-active`);
            setRenderDOM(false); }}); }}); }, [active, name]);if(! renderDOM) {return null;
  }

  return cloneElement(Children.only(children), {
    ref: nodeRef
  });
};
Copy the code

This component receives three props, respectively

  • Children need to ReactElement the transition animation, allowing only one Element to be passed
  • Name The CSS class name prefix of the transition animation
  • Active Boolean value used to distinguish between entry and disappearance

Usage:

<CSSTransition name="fade" active={active}>
    // a ReactElement that needs to animate the transition
</CssTransition>
Copy the code

Stagger effect with transition-delay:

The complete sample code is here, note that 📢 this is only a quick implementation for demonstration examples, there are a lot of problems are not considered, can only be used for learning reference.

conclusion

Originally thought very basic simple knowledge point, minute can write this article. I didn’t think it took me a lot of time to check the documents and make the DEMO. Fortunately, a lot of knowledge points have been clarified in the process of sorting out data. Hopefully this article has helped you familiarize yourself with Transition animation ❤️.