Search code examples
htmlcssreactjsfrontendmarquee

How to make a vertical marquee effect displaying a list in React


I'm trying to make an marquee effect displaying a list of items on a vertical auto-scroll. At the end of the rendered list, I want the list to appear again right after the last item to make a continuous animation.

The code below is what I have so far. I basically made an animation that runs in 25 seconds, and then after each 25 seconds I added all items in the list back to it using React setState.

The code I tried:

import styled, { keyframes } from "styled-components";
import React from "react";

// Keyframes for the animation
const marqueeTop = keyframes`
  0% {
    top: 0%;
  }
  100% {
    top: -100%;
  }
`;

// Styled components
const MarqueeWrapper = styled.div`
  overflow: hidden;
  margin: 0 auto !important;
`;

const MarqueeBlock = styled.div`
  width: 100%;
  height: 44vh;
  flex: 1;
  overflow: hidden;
  box-sizing: border-box;
  position: relative;
  float: left;
`;

const MarqueeInner = styled.div`
  position: relative;
  display: inline-block;
  animation: ${marqueeTop} 25s linear infinite;
  animation-timing-function: linear;
  &:hover {
    animation-play-state: paused; /* Pause the animation on hover */
  }
`;

const MarqueeItem = styled.div`
  transition: all 0.2s ease-out;
`;

export default class MarqueeContainer extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      itemsToShow: this.props.items, //gets items from parent
    };
  }

  // Add the same list of items back for re-rendering
  scrollItems = () => {
    setInterval(() => {
      this.setState((prevState) => {
        const newItems = [...prevState.itemsToShow];
        newItems.push(this.props.items);
        return { itemsToShow: newItems };
      });
    }, 25000);
  };

  componentDidMount() {
    this.scrollItems();
  }

  render() {
    return (
      <MarqueeWrapper>
        <MarqueeBlock>
          <MarqueeInner>
            {this.state.itemsToShow.map((item, index) => (
              <MarqueeItem key={index}>{item}</MarqueeItem>
            ))}
          </MarqueeInner>
        </MarqueeBlock>
      </MarqueeWrapper>
    );
  }
}

That kinda worked, but I noticed that after each 25-second period, the list "refreshes" and the animation restarts at the first item of the list, which I think is the default behavior of css, but it looks weird. Is there anyway to achieve the desired animation? I'm open to any new method.

Thanks a lot!

Edit 1: Here is a demonstration of how it currently works. For a list with 39 items, I want item 0 to appear right after item 39, but instead the list refreshes and item 0 appears on top. Demonstration GIF

Edit 2:

Here is the code that works best for me, which is a combination of MrSrv7's code below and some of my code which readd the items back to the array after a period of time:

import styled, { keyframes } from "styled-components";
import React from "react";

const marqueeTop = keyframes`
  0% {
    transform: translateY(0);
  }
  100% {
    transform: translateY(-100%);
  }
`;

const MarqueeWrapper = styled.div`
  overflow: hidden;
  margin: 0 auto !important;
`;

const MarqueeBlock = styled.div`
  width: 100%;
  height: 44vh;
  flex: 1;
  overflow: hidden;
  box-sizing: border-box;
  position: relative;
  float: left;
`;

const MarqueeInner = styled.div`
  position: relative;
  display: inline-block;
  animation: ${marqueeTop} 120s linear infinite;
  animation-timing-function: linear;
  &:hover {
    animation-play-state: paused;
  }
`;

const MarqueeItem = styled.div`
  transition: all 1s ease-out;
`;

export default class MarqueeContainer extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      itemsToShow: this.generateMarqueeItems(),
    };
  }

  scrollItems = () => {
    setInterval(() => {
      this.setState({ itemsToShow: this.generateMarqueeItems() });
    }, 120000);
  };

  componentDidMount() {
    this.scrollItems();
  }

  generateMarqueeItems = () => {
    const items = this.props.items;
    const visibleItemsCount = 5;
    const marqueeItems = Array.from({ length: visibleItemsCount }, () => items).flat();

    return marqueeItems;
  };

  render() {
    return (
      <MarqueeWrapper>
        <MarqueeBlock>
          <MarqueeInner>{this.state.itemsToShow && this.state.itemsToShow.map((item, index) => <MarqueeItem key={index}>{item}</MarqueeItem>)}</MarqueeInner>
        </MarqueeBlock>
      </MarqueeWrapper>
    );
  }
}


Solution

  • After some trial and error, I came up with the following. I didn't use the setInterval. I created a function that keeps adding to the state.itemsToShow and mapping the same.

    import styled, { keyframes } from "styled-components";
    import React from "react";
    
    const marqueeTop = keyframes`
      0% {
        transform: translateY(0);
      }
      100% {
        transform: translateY(-100%);
      }
    `;
    
    const MarqueeWrapper = styled.div`
      overflow: hidden;
      margin: 0 auto !important;
    `;
    
    const MarqueeBlock = styled.div`
      width: 100%;
      height: 44vh;
      flex: 1;
      overflow: hidden;
      box-sizing: border-box;
      position: relative;
      float: left;
    `;
    
    const MarqueeInner = styled.div`
      position: relative;
      display: inline-block;
      animation: ${marqueeTop} 25s linear infinite;
      animation-timing-function: linear;
      &:hover {
        animation-play-state: paused;
      }
    `;
    
    const MarqueeItem = styled.div`
      transition: all 0.2s ease-out;
    `;
    
    export default class MarqueeContainer extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          itemsToShow: this.generateMarqueeItems()
        };
      }
    
      generateMarqueeItems = () => {
        const { items } = this.props;
        const visibleItemsCount = 5; // Adjust this value to control the number of visible items in the marquee
        const marqueeItems = [];
    
        // Keep adding items to the list until it is long enough to fill the scrolling area seamlessly
        while (marqueeItems.length < items.length * visibleItemsCount) {
          marqueeItems.push(...items);
        }
    
        return marqueeItems;
      };
    
      render() {
        const { itemsToShow } = this.state;
        return (
          <MarqueeWrapper>
            <MarqueeBlock>
              <MarqueeInner>
                {itemsToShow &&
                  itemsToShow.length > 0 &&
                  itemsToShow.map((item, index) => (
                    <MarqueeItem key={index}>{item}</MarqueeItem>
                  ))}
              </MarqueeInner>
            </MarqueeBlock>
          </MarqueeWrapper>
        );
      }
    }
    

    Hope this helps