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>
);
}
}
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