Introducing Nuvyx UI v1.0.0

Marquee

A customizable, interactive scrolling marquee component with various animation options, drag capabilities, and responsive design.

Marquee

React
Motion
React
Motion

Click or drag the marquee to interact with it

Installation Guide

1

Install Dependencies

Framer Motion

npm install framer-motion

Utility Functions

npm install clsx tailwind-merge
2

Setup Configuration

Create file: /lib/utils.ts
/lib/utils.ts
Configuration
1import { clsx, type ClassValue } from "clsx";
2import { twMerge } from "tailwind-merge";
3
4export function cn(...inputs: ClassValue[]) {
5return twMerge(clsx(inputs));
6}
3

Copy Component Code

Marquee.tsx
TypeScript
1"use client";
2import { cn } from "@/lib/utils";
3import { useMotionValue, animate, motion } from "framer-motion";
4import { useState, useEffect, useRef } from "react";
5
6export type MarqueeProps = {
7  children: React.ReactNode;
8  gap?: number;
9  speed?: number;
10  speedOnHover?: number;
11  direction?: "horizontal" | "vertical";
12  reverse?: boolean;
13  className?: string;
14  fadeEdges?: boolean;
15  fadeWidth?: number;
16  pauseOnTap?: boolean;
17  draggable?: boolean;
18};
19
20export function Marquee({
21  children,
22  gap = 16,
23  speed = 100,
24  speedOnHover,
25  direction = "horizontal",
26  reverse = false,
27  className,
28  fadeEdges = false,
29  fadeWidth = 64,
30  pauseOnTap = true,
31  draggable = true,
32}: MarqueeProps) {
33  const [currentSpeed, setCurrentSpeed] = useState(speed);
34  const [isPaused, setIsPaused] = useState(false);
35  const [isDragging, setIsDragging] = useState(false);
36  const containerRef = useRef<HTMLDivElement>(null);
37  const translation = useMotionValue(0);
38  const [isTransitioning, setIsTransitioning] = useState(false);
39  const [key, setKey] = useState(0);
40  const dragStartPosition = useRef(0);
41  const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
42
43  useEffect(() => {
44    if (!containerRef.current) return;
45
46    const currentRef = containerRef.current;
47
48    const updateDimensions = () => {
49      if (currentRef) {
50        const rect = currentRef.getBoundingClientRect();
51        setDimensions({
52          width: rect.width,
53          height: rect.height,
54        });
55      }
56    };
57
58    updateDimensions();
59
60    const resizeObserver = new ResizeObserver(updateDimensions);
61    resizeObserver.observe(currentRef);
62
63    return () => {
64      resizeObserver.unobserve(currentRef);
65      resizeObserver.disconnect();
66    };
67  }, []);
68
69  const { width, height } = dimensions;
70
71  useEffect(() => {
72    let controls;
73
74    if (isPaused || isDragging || (!width && !height)) {
75      return () => {};
76    }
77
78    const size = direction === "horizontal" ? width : height;
79    const contentSize = size + gap;
80
81    if (!size) return () => {};
82
83    const from = reverse ? -contentSize / 2 : 0;
84    const to = reverse ? 0 : -contentSize / 2;
85    const distanceToTravel = Math.abs(to - from);
86    const duration = distanceToTravel / currentSpeed;
87
88    if (isTransitioning) {
89      const remainingDistance = Math.abs(translation.get() - to);
90      const transitionDuration = remainingDistance / currentSpeed;
91
92      controls = animate(translation, [translation.get(), to], {
93        ease: "linear",
94        duration: transitionDuration,
95        onComplete: () => {
96          setIsTransitioning(false);
97          setKey((prevKey) => prevKey + 1);
98        },
99      });
100    } else {
101      controls = animate(translation, [from, to], {
102        ease: "linear",
103        duration: duration,
104        repeat: Infinity,
105        repeatType: "loop",
106        repeatDelay: 0,
107        onRepeat: () => {
108          translation.set(from);
109        },
110      });
111    }
112
113    return controls?.stop;
114  }, [
115    key,
116    translation,
117    currentSpeed,
118    width,
119    height,
120    gap,
121    isTransitioning,
122    direction,
123    reverse,
124    isPaused,
125    isDragging,
126  ]);
127
128  const fadeGradientStyles = (() => {
129    if (!fadeEdges) return {};
130
131    const size = direction === "horizontal" ? width : height;
132    if (size === 0) return {};
133
134    const fadePercentage = Math.min(100, Math.round((fadeWidth / size) * 100));
135
136    if (direction === "horizontal") {
137      return {
138        maskImage: `linear-gradient(to right, transparent, black ${fadePercentage}%, black ${
139          100 - fadePercentage
140        }%, transparent 100%)`,
141        WebkitMaskImage: `linear-gradient(to right, transparent, black ${fadePercentage}%, black ${
142          100 - fadePercentage
143        }%, transparent 100%)`,
144      };
145    } else {
146      return {
147        maskImage: `linear-gradient(to bottom, transparent, black ${fadePercentage}%, black ${
148          100 - fadePercentage
149        }%, transparent 100%)`,
150        WebkitMaskImage: `linear-gradient(to bottom, transparent, black ${fadePercentage}%, black ${
151          100 - fadePercentage
152        }%, transparent 100%)`,
153      };
154    }
155  })();
156
157  const handleTap = () => {
158    if (pauseOnTap && !isDragging) {
159      setIsPaused(!isPaused);
160      setIsTransitioning(true);
161      setKey((prevKey) => prevKey + 1);
162    }
163  };
164
165  const hoverProps = speedOnHover
166    ? {
167        onHoverStart: () => {
168          setIsTransitioning(true);
169          setCurrentSpeed(speedOnHover);
170        },
171        onHoverEnd: () => {
172          setIsTransitioning(true);
173          setCurrentSpeed(speed);
174        },
175      }
176    : {};
177
178  const handleDragStart = () => {
179    if (!draggable) return;
180
181    setIsDragging(true);
182    dragStartPosition.current = translation.get();
183  };
184
185  const handleDragEnd = () => {
186    if (!draggable) return;
187
188    setIsDragging(false);
189    setIsTransitioning(true);
190    setKey((prevKey) => prevKey + 1);
191  };
192
193  const dragConstraints = (() => {
194    const contentSize = direction === "horizontal" ? width : height;
195
196    return {
197      left: -contentSize,
198      right: contentSize,
199      top: -contentSize,
200      bottom: contentSize,
201    };
202  })();
203
204  return (
205    <div
206      className={cn(
207        "overflow-hidden relative",
208        className,
209        (pauseOnTap || draggable) && "cursor-pointer",
210        isDragging && "cursor-grabbing"
211      )}
212      style={fadeGradientStyles}
213      onClick={handleTap}
214    >
215      <motion.div
216        className={cn("flex w-max", draggable && "cursor-grab")}
217        style={{
218          ...(direction === "horizontal"
219            ? { x: translation }
220            : { y: translation }),
221          flexDirection: direction === "horizontal" ? "row" : "column",
222          gap: `${gap}px`,
223        }}
224        ref={containerRef}
225        {...hoverProps}
226        drag={draggable ? (direction === "horizontal" ? "x" : "y") : false}
227        dragConstraints={dragConstraints}
228        onDragStart={handleDragStart}
229        onDragEnd={handleDragEnd}
230        dragElastic={0.1}
231        dragMomentum={false}
232      >
233        {children}
234        {children}
235      </motion.div>
236    </div>
237  );
238}
239
4

Final Steps

Update Import Paths

Make sure to update the import paths in the component code to match your project structure. For example, change @/components/ui/button to match your UI components location.

Props

NameTypeDefaultDescription
childrenReact.ReactNodeundefinedContent to be displayed in the marquee
gapnumber16Space between repeated content in pixels
speednumber100Animation speed (higher is faster)
speedOnHovernumberundefinedAnimation speed when hovered (higher is faster)
directionstringhorizontalScrolling direction ('horizontal' or 'vertical')
reversebooleanfalseReverse the animation direction
classNamestringundefinedAdditional CSS classes to apply
fadeEdgesbooleanfalseFade the edges of the marquee content
fadeWidthnumber64Width of the edge fade in pixels
pauseOnTapbooleantruePause animation when clicked
draggablebooleantrueAllow dragging interaction with the marquee

Examples

avenger logobatman logoblack panther logocaptain america logodaredevil logodeadpool logoavenger logobatman logoblack panther logocaptain america logodaredevil logodeadpool logo
flash logogreen lantern logoironman logosuperman logodr strange logoshield logoflash logogreen lantern logoironman logosuperman logodr strange logoshield logo
DC logoBandai logoDisney logoWarner Bros logoMarvel logoDC logoBandai logoDisney logoWarner Bros logoMarvel logo