javascriptreactjsreactcsstransitiongroup

Layout breaks on react-transition-group animation


I am trying to get familiar with react-transition-group but having some issues. In a simple component that displays current counter and animates the displayed value on increment or decrement when a new value enter, it shifts the previous one which creates abrupt effect. It probably happens because a new element with opacity 0 enters the dome and both values are displayed at the same time.

The whole component looks like this:

import React, { Component } from "react";
import { TransitionGroup, CSSTransition } from "react-transition-group";

import "./App.css";

class App extends Component {
  state = {
    progress: 0
  };
  incr = () => {
    this.setState({
      progress: this.state.progress + 1
    });
  };
  decr = () => {
    this.setState({
      progress: this.state.progress - 1
    });
  };
  render() {
    return (
      <div className="App">
        <TransitionGroup>
          <CSSTransition
            in={true}
            appear={true}
            key={this.state.progress}
            timeout={2000}
            classNames="slide"
          >
            <div key={this.state.progress}>{this.state.progress}</div>
          </CSSTransition>
        </TransitionGroup>
        <button onClick={this.decr}>decr</button>
        <button onClick={this.incr}>incr</button>
      </div>
    );
  }
}

export default App;

Transition classes look like this:

/* slide enter */
.slide-enter {
  opacity: 0;
  transform: scale(0.97) translateX(5px);
  z-index: 1;
}
.slide-enter.slide-enter-active {
  opacity: 1;
  transform: scale(1) translateX(0);
  transition: opacity 3000ms linear 1000ms, transform 3000ms ease-in-out 1000ms;
}

/* slide exit */
.slide-exit {
  opacity: 1;
  transform: scale(1) translateX(0);
}
.slide-exit.slide-exit-active {
  opacity: 0;
  transform: scale(0.97) translateX(5px);
  transition: opacity 1500ms linear, transform 1500ms ease-out;
}
.slide-exit-done {
  opacity: 0;
}

I want to achieve the following behavior: on increment/decrement current displayed counter fades away, and new one fades in after the current one is not already there.


Solution

  • It probably happens because a new element with opacity 0 enters the dome and both values are displayed at the same time.

    Yes, that's exactly it. You can use position: absolute to remove the elements from the document's flow, and you may also need to somehow compensate for the fact that those elements are removed from the document's flow.

    <TransitionGroup>
      <CSSTransition
        in={true}
        appear={true}
        key={this.state.progress}
        timeout={2000}
        classNames="slide"
      >
        <div className="animating" key={this.state.progress}>
          {this.state.progress}
        </div>
      </CSSTransition>
    </TransitionGroup>
    

    CSS

    .animating {
      position: absolute;
    }
    
    button:first-of-type {
      margin-top: 50px;
    }
    

    Without React Transition Group

    JS

    import React, { Component } from "react";
    import "./App.css";
    
    class App extends Component {
      state = {
        progress: 0,
        transition: "entering" /* entering | exiting */
      };
      instanceVariables = {
        temporaryProgress: 0,
        timeoutId: null
      };
      animate = () => {
        clearTimeout(this.instanceVariables.timeoutId);
        this.instanceVariables.timeoutId = setTimeout(() => {
          this.setState({
            progress: this.instanceVariables.temporaryProgress,
            transition: "entering"
          });
        }, 1000);
        this.setState({
          transition: "exiting"
        });
      };
      incr = () => {
        this.instanceVariables.temporaryProgress += 1;
        this.animate();
      };
      decr = () => {
        this.instanceVariables.temporaryProgress -= 1;
        this.animate();
      };
      render() {
        return (
          <div className="App">
            <div className={`animate ${this.state.transition}`}>
              {this.state.progress}
            </div>
            <button onClick={this.decr}>decr</button>
            <button onClick={this.incr}>incr</button>
          </div>
        );
      }
    }
    
    export default App;
    

    CSS

    .animate {
      transition: opacity 1s linear, transform 1s ease-in-out;
    }
    
    .animate.exiting {
      transform: translateX(20px);
      opacity: 0;
    }
    
    .animate.entering {
      transform: translateX(0);
      opacity: 1;
    }