Search code examples
reactjsreact-spring

React Spring - Animate list - one item in list at a time


Given a list:

li {
  display: inline-block;
  color: #000000;
}

<ul>
  <li>Item 1</li>
  <li>Item 2</li>
  <li>Item 3</li>
  <li>Item 4</li>
</ul>

Using react-spring, I am trying to animate the colour of each item in a list (one by one) every 3 seconds and loop from start to finish.

For example:

  1. From - Color starts as black
  2. Enter - Color changes to red
  3. Leave - Color changes back to black

I can get an individual item to show and colour to update and then hides (as only 1 item from the list is being animated), but not the whole list to show and change the colour of each item 1 by 1.

const ColourListTransition = (items, delay) => {
  const [index, setIndex ] = useState(0);
  useEffect(() => {
    const interval = setInterval(() => {
      setIndex((state) => ( state + 1 ) % items.length);
    }, delay);

    return () => clearInterval(interval);
  }, []);

  return useTransition(items[index], {
    from: { color: '#000000' },
    enter: { color: "#FF0000" },
    leave: { color: "#000000" },
    loop: true,
    config: config.molasses
  })
}

{ ColourListTransition(['item 1', 'item 2', 'item 3', 'item 4'], 3000)(({ color }, item) => (
  <animated.li
    key={ item }
    style={ { color, display: 'inline-block', listStyleType: 'none' } }
  >
    { item }
  </animated.li>
)) }

Solution

  • I played around with useTransition but I just could not figure it out. It was easy when I switched to using useSprings instead. So this might not be the most elegant solution but it works.

    We create an array of spring animations which correspond to the items in your list. The color of each item is based on whether its index matches the active index from state. enter and leave don't really come into play here since you always have same 4 items in your array. I kept your useState and useEffect hooks the same.

    import React, { useState, useEffect } from "react";
    import { animated, config, useSprings } from "@react-spring/web";
    
    const ColourListTransition = ({delay, items, activeColor, inactiveColor}) => {
      const [index, setIndex] = useState(0);
    
      useEffect(() => {
        const interval = setInterval(() => {
          setIndex((state) => (state + 1) % items.length);
        }, delay);
    
        return () => clearInterval(interval);
      }, []);
    
      const [springs] = useSprings(
        // count
        items.length,
        // properties as a function of index
        (i) => ({
          // determine the current color based on the index
          color: i === index ? activeColor : inactiveColor,
          // all items start out black
          from: { color: inactiveColor },
          config: config.molasses
        }),
        // dependency on index state
        [index]
      );
    
      return (
        <ul>
          {springs.map(({ color }, i) => (
            <animated.li
              key={items[i]}
              style={{ color, display: "inline-block", listStyleType: "none" }}
            >
              {items[i]}
            </animated.li>
          ))}
        </ul>
      );
    };
    
    export default function App() {
      return (
        <ColourListTransition
          items={["item 1", "item 2", "item 3", "item 4"]}
          delay={3000}
          activeColor={"#FF0000"}
          inactiveColor={"#000000"}
        />
      );
    }
    

    CodeSandbox Link