logo

Create a Marquee Component with React

Since forever, I have always liked a website when it has a slider listing the brands using the said website.

I don't have a lot of experience designing landing pages, which is why I didn't even know what this slider thingy is called, until now.

When I was moving around the internet, I finally saw what these sliders are called, they are called Marquee!

After learning the term, I checked out a couple of libraries and how they do these animations. Animation is my weakest point so even a simple animation like this is black magic to me.

Since the best way to learn is by doing it yourself, I created a simple Marquee component.

In this blog post, I will show you how you can create a Marquee component using React by explaining the magic behind it.

If you don't have time and need a component fast, check out react-fast-marquee library. It is small and covers a lot of cases you might need.

Marquee Anatomy

When we are designing our Marquee component, the number one important thing is the non-stop movement of the children.

We need to make sure that the children are moving from right to left and when they reach the end, they should start from the beginning.

To do that, we need to fill the container with enough of our children and add an extra children at the end to make sure the animation is seamless.

The above image shows the initial state.

When the animation starts, the children will move from right to left. When the first child reaches the end, we will move the children to the beginning.

Since we have a second children, the container is never empty.

When the first children reaches the end, we will move the children to the beginning.

With this logic, there will be always children moving from right to left.

Marquee Component

I will give you the component first and explain what is hapenning in each line of code after.

1"use client";
2
3import { motion } from "framer-motion";
4import { Children, Fragment, ReactNode, useCallback, useEffect, useRef, useState } from "react";
5import { twMerge } from "tailwind-merge";
6
7type Props = {
8 children: ReactNode;
9 duration?: number;
10 containerClassName?: string;
11};
12
13const Marquee = ({ children, duration, containerClassName }: Props) => {
14 const [isMounted, setIsMounted] = useState(false);
15 const containerRef = useRef<HTMLDivElement>(null);
16 const marqueeRef = useRef<HTMLDivElement>(null);
17 const [multiplier, setMultiplier] = useState(1);
18
19 const calculateWidth = useCallback(() => {
20 const containerRect = containerRef.current?.getBoundingClientRect();
21 const marqueeRect = marqueeRef.current?.getBoundingClientRect();
22 const containerWidth = containerRect?.width;
23 const marqueeWidth = marqueeRect?.width;
24
25 if (containerWidth && marqueeWidth) {
26 setMultiplier(marqueeWidth < containerWidth ? Math.ceil(containerWidth / marqueeWidth) : 1);
27 }
28 }, [containerRef]);
29
30 useEffect(() => {
31 if (!isMounted) return;
32 calculateWidth();
33 if (marqueeRef.current && containerRef.current) {
34 const resizeObserver = new ResizeObserver(() => calculateWidth());
35 resizeObserver.observe(marqueeRef.current);
36 resizeObserver.observe(containerRef.current);
37 return () => {
38 if (!resizeObserver) return;
39 resizeObserver.disconnect();
40 };
41 }
42 }, [calculateWidth, containerRef, isMounted]);
43
44 useEffect(() => {
45 calculateWidth();
46 }, [children, calculateWidth]);
47
48 useEffect(() => {
49 setIsMounted(true);
50 }, []);
51
52 const multiplyChildren = useCallback(
53 (multiplier: number) => {
54 const arraySize = multiplier >= 0 ? multiplier : 0;
55 return [...Array(arraySize)].map((_, i) => (
56 <Fragment key={i}>
57 {Children.map(children, (child) => (
58 <div>{child}</div>
59 ))}
60 </Fragment>
61 ));
62 },
63 [children]
64 );
65
66 const scroll = {
67 x: ["0%", "-100%"],
68 transition: {
69 duration: duration || 10,
70 ease: "linear",
71 repeat: Infinity,
72 },
73 };
74
75 if (!isMounted) return null;
76
77 return (
78 <div
79 ref={containerRef}
80 className={twMerge("overflow-x-hidden flex flex-row relative w-full", containerClassName)}
81 >
82 <motion.div
83 animate={scroll}
84 className="flex-shrink-0 flex-grow-0 basis-auto min-w-full flex flex-row items-center"
85 >
86 <div
87 ref={marqueeRef}
88 className="flex-shrink-0 flex-grow-0 basis-auto flex min-w-fit flex-row items-center"
89 >
90 {Children.map(children, (child) => (
91 <div>{child}</div>
92 ))}
93 </div>
94 {multiplyChildren(multiplier - 1)}
95 </motion.div>
96 <motion.div
97 animate={scroll}
98 className="flex-shrink-0 flex-grow-0 basis-auto min-w-full flex flex-row items-center"
99 >
100 {multiplyChildren(multiplier)}
101 </motion.div>
102 </div>
103 );
104};
105
106export { Marquee };

Use Client Directive

1"use client";

This directive is used to tell the bundler that this file is only for the client-side. This way, the bundler will not include this file in the server-side bundle.

If you are not using React Server Components, you can remove this directive.

Imports

1import { motion } from "framer-motion";
2import { Children, Fragment, ReactNode, useCallback, useEffect, useRef, useState } from "react";
3import { twMerge } from "tailwind-merge";

I am trying to learn framer-motion. That is why I tried to do this animation with framer-motion.

It is not necessary to use framer-motion, you can use CSS animations or any other animation library you like.

I will also use Tailwind for styling. If you don't use Tailwind, you can remove the twMerge function and use your own styling.

Props

1type Props = {
2 children: ReactNode;
3 duration?: number;
4 containerClassName?: string;
5};

This component doesn't have a lot of props. However, you can extend it to add more props like speed, direction, etc.

States

1const Marquee = ({ children, duration, containerClassName }: Props) => {
2 const [isMounted, setIsMounted] = useState(false);
3 const containerRef = useRef<HTMLDivElement>(null);
4 const marqueeRef = useRef<HTMLDivElement>(null);
5 const [multiplier, setMultiplier] = useState(1);

isMounted: This state is used to check if the component is mounted or not. Even if we added "use client" directive, the component will still render on server. With this state, we can make sure that the component is rendered only on the client-side.

containerRef: This ref is used to get the container's width.

marqueeRef: This ref is used to get the marquee's width.

multiplier: This state is used to calculate how many children we need to fill the container.

Calculate Width

1 const calculateWidth = useCallback(() => {
2 const containerRect = containerRef.current?.getBoundingClientRect();
3 const marqueeRect = marqueeRef.current?.getBoundingClientRect();
4 const containerWidth = containerRect?.width;
5 const marqueeWidth = marqueeRect?.width;
6
7 if (containerWidth && marqueeWidth) {
8 setMultiplier(marqueeWidth < containerWidth ? Math.ceil(containerWidth / marqueeWidth) : 1);
9 }
10 }, [containerRef]);

This function is the most important part of the component. It calculates how many children we need to fill the container.

It uses getBoundingClientRect to get the width of the container and marquee.

We are using Math.ceil to make sure children width is always bigger than or the same as the container width.

Resize Observer

1 useEffect(() => {
2 if (!isMounted) return;
3 calculateWidth();
4 if (marqueeRef.current && containerRef.current) {
5 const resizeObserver = new ResizeObserver(() => calculateWidth());
6 resizeObserver.observe(marqueeRef.current);
7 resizeObserver.observe(containerRef.current);
8 return () => {
9 if (!resizeObserver) return;
10 resizeObserver.disconnect();
11 };
12 }
13 }, [calculateWidth, containerRef, isMounted]);

When the user updates the screen size, we need to recalculate the width of the container and marquee.

That is why we are using ResizeObserver to listen to the changes in the container and marquee.

Calculate Width on Children Change

1 useEffect(() => {
2 calculateWidth();
3 }, [children, calculateWidth]);

When the children change, we need to recalculate the width of the container and marquee.

Set isMounted

1 useEffect(() => {
2 setIsMounted(true);
3 }, []);

When the component is mounted, we set the isMounted state to true.

Multiply Children

1 const multiplyChildren = useCallback(
2 (multiplier: number) => {
3 const arraySize = multiplier >= 0 ? multiplier : 0;
4 return [...Array(arraySize)].map((_, i) => (
5 <Fragment key={i}>
6 {Children.map(children, (child) => (
7 <div>{child}</div>
8 ))}
9 </Fragment>
10 ));
11 },
12 [children]
13 );

This function is used to multiply the children. We are using this function to fill the container with enough children.

It uses Children to map over the children and create a new array with the same children.

Scroll Animation

1 const scroll = {
2 x: ["0%", "-100%"],
3 transition: {
4 duration: duration || 10,
5 ease: "linear",
6 repeat: Infinity,
7 },
8 };

This object is used to animate the children. We are using framer-motion's motion component to animate the children.

You can change ease to other values and utilize framer-motion's springy animations.

Return

1 if (!isMounted) return null;
2
3 return (
4 <div
5 ref={containerRef}
6 className={twMerge("overflow-x-hidden flex flex-row relative w-full", containerClassName)}
7 >
8 <motion.div
9 animate={scroll}
10 className="flex-shrink-0 flex-grow-0 basis-auto min-w-full flex flex-row items-center"
11 >
12 <div
13 ref={marqueeRef}
14 className="flex-shrink-0 flex-grow-0 basis-auto flex min-w-fit flex-row items-center"
15 >
16 {Children.map(children, (child) => (
17 <div>{child}</div>
18 ))}
19 </div>
20 {multiplyChildren(multiplier - 1)}
21 </motion.div>
22 <motion.div
23 animate={scroll}
24 className="flex-shrink-0 flex-grow-0 basis-auto min-w-full flex flex-row items-center"
25 >
26 {multiplyChildren(multiplier)}
27 </motion.div>
28 </div>
29 );
30};

If the component is not mounted, we return null.

Here we have two motion.div components. There should be always children in the container view to make the animation seamless.

We are using twMerge to merge the container's class name with the given class name.

We are using motion.div to animate the children.

We are using multiplyChildren to fill the container with enough children.

The reason why the first multiplyChildren is minus 1 is that we already have one set of children in the marquee.

Conclusion

Creating a Marquee component is not that hard. You can create a simple Marquee component using nothing but vanilla JavaScript and CSS.

However, if you want to add animations and make it more interactive, you can use libraries like framer-motion.

If you have any questions or need further assistance, feel free to email me.