Search code examples
cssreactjstransition

CSS transition not shown in React component


The component should satisfy the following:

  1. Elements in a list should be displayed, one element at a time.
  2. There should be two buttons: Prev and Next, to display previous/next element of the list.
  3. When the Back/Next button is clicked, there should be some sort of sliding effect to indicate that the previous/next item is coming.
    1. The direction of the sliding effect is depending on which button is clicked. Next: right-to-left, Prev: left-to-right.
    2. When the first item is first rendered, there should be no sliding effects.

I'm stuck in the third step, where the CSS is not working as expected.

The sandbox is here: https://codesandbox.io/s/gracious-kapitsa-cxtsn6

This is the CSS properties for the expected sliding transition:

const styleSlideRtoL = {
  position: "relative",
  left: "150px",
  transition: "transform 0.3s ease-in-out",
  transform: "translateX(-150px)",
  color: "blue",
};

They are used in the list elements as follows:

const div1 = <div style={styleSlideRtoL}>Lorem Ipsum 1</div>;
const div2 = <div style={styleSlideRtoL}>Lorem Ipsum 2</div>;
const div3 = <div style={styleSlideRtoL}>Lorem Ipsum 3</div>;
const div4 = <div style={styleSlideRtoL}>Lorem Ipsum 4</div>;
const div5 = <div style={styleSlideRtoL}>Lorem Ipsum 5</div>;
const divs = [div1, div2, div3, div4, div5];

With the above code, there is no transition effects observed.

However, if I use a different style (no sliding effect) for the 1st, 3rd, and 5th div, then the 2nd and 4th div will get the sliding effect as expected.

const styleNoSlide = {
  // position: "relative",
  // left: "150px",
  // transition: "transform 0.3s ease-in-out",
  // transform: "translateX(-150px)",
  color: "orange",
}
const div1 = <div style={styleNoSlide}>Lorem Ipsum 1</div>;
const div2 = <div style={styleSlideRtoL}>Lorem Ipsum 2</div>;
const div3 = <div style={styleNoSlide}>Lorem Ipsum 3</div>;
const div4 = <div style={styleSlideRtoL}>Lorem Ipsum 4</div>;
const div5 = <div style={styleNoSlide}>Lorem Ipsum 5</div>;

Questions:

  1. Why is there no sliding effects when I use styleSlideRtoL for all the divs?
  2. How to implement the sliding effect properly, with minimal changes to the existing code?
  3. When alternate styles are used, the 1st, 3rd, and 5th is rendered 1px higher than the 2nd and 4th div. Why?

Solution

  • Firstly, simply mounting a DOM element that has the transition CSS property will not cause the "enter" animation to happen. For transition to animate something an element has to first be loaded with one set of CSS properties, then change to a new set of CSS properties. Inserting a <div> with the CSS properties in their final state won't cause any animation to happen.

    So then why does the latter example half-work? When you are switching between these divs by updating the selectedIndex state, React is using its reconciliation process, which tries hard to minimise the amount of DOM operations to change from the previous DOM state to the next one. When you change from the first to the second <div>, React is actually just patching the existing <div> by changing its attributes to the new passed styles.

    Suddenly, this satisfies the requirements of transition that a DOM node's CSS properties must change and not just be inserted with the final state.

    You can demonstrate this by forcing React's algorithm to not reuse the same base <div> element by giving each of them a unique key:

      const div1 = <div key={1} style={styleNoSlide}>Lorem Ipsum 1</div>;
      const div2 = <div key={2} style={styleSlideRtoL}>Lorem Ipsum 2</div>;
      const div3 = <div key={3} style={styleNoSlide}>Lorem Ipsum 3</div>;
      const div4 = <div key={4} style={styleSlideRtoL}>Lorem Ipsum 4</div>;
      const div5 = <div key={5} style={styleNoSlide}>Lorem Ipsum 5</div>;
    

    You will notice now that React is treating them as unique elements, the animation is completely gone again.

    The easiest way you could achieve what you actually want is to instead of swapping between divs, render all of the divs all of the time, but make it so only one is actually visible (the rest will overflow out of view). Then adjust the transform position in response to clicking the buttons.

    import "./styles.css";
    import { useState } from "react";
    
    export default function App() {
      const [selectedIndex, setSelectedIndex] = useState(0);
    
      const styleSlideWrapper = {
        width: "150px",
        overflow: "hidden",
        border: "3px solid magenta",
        borderRadius: "6px",
        whiteSpace: "nowrap"
      };
    
      const styleSlideRtoL = {
        position: "relative",
        color: "blue",
        display: "inline-block",
        width: "100%",
        transition: "transform 0.3s ease-in-out",
        transform: `translateX(-${100 * selectedIndex}%)`
      };
    
      const div1 = <div style={styleSlideRtoL}>Lorem Ipsum 1</div>;
      const div2 = <div style={styleSlideRtoL}>Lorem Ipsum 2</div>;
      const div3 = <div style={styleSlideRtoL}>Lorem Ipsum 3</div>;
      const div4 = <div style={styleSlideRtoL}>Lorem Ipsum 4</div>;
      const div5 = <div style={styleSlideRtoL}>Lorem Ipsum 5</div>;
    
      const divs = [div1, div2, div3, div4, div5];
    
      const handleBack = () => {
        if (selectedIndex > 0) {
          setSelectedIndex(selectedIndex - 1);
        }
      };
      const handleNext = () => {
        if (selectedIndex < divs.length - 1) {
          setSelectedIndex(selectedIndex + 1);
        }
      };
      return (
        <>
          <h1>Click a button to change item.</h1>
          <div style={styleSlideWrapper}>{divs}</div>
          <button onClick={() => handleBack()} disabled={selectedIndex === 0}>
            Prev
          </button>
          &nbsp;
          <button
            onClick={() => handleNext()}
            disabled={selectedIndex === divs.length - 1}
          >
            Next
          </button>
        </>
      );
    }
    
    

    Working sandbox.

    Note key isn't needed here because we are rendering all of the divs and not switching between them so the problem with React patching a single div never occurs anyway.

    The key reason for doing this though is because you want to show the previous one exiting as well. As well as that, the elements are mounted when the page loads, and then the transform is adjusted after which satisfies the earlier outline of how transition works.

    Worth noting many great animation libraries make life easier like framer-motion for example.