Docs
Blocks
Simple Marquee

Simple Marquee

A simple marquee component for scrolling HTML elements.

Weekly Finds

Image 1
Image 2
Image 3
Image 4
Image 5
Image 5
Image 6
Image 7
Image 8
Image 9
Image 10
Image 11
Image 12
Image 13
Image 14

Artworks from Cosmos.

Credits


This component is inspired by this scroll example by Motion.

Installation


1pnpm dlx shadcn@latest add "https://fancycomponents.dev/r/simple-marquee.json"

Usage


You only need to wrap your elements with the SimpleMarquee component, everything else is taken care of by the component itself.

Understanding the component


Unlike most marquee implementations that use simple CSS animations, this component uses Motion's useAnimationFrame hook to provide more control over the animation. This allows for a bunch of fancy effects, such as:

  • Changing velocity and direction by dragging
  • Adjusting speed in response to scrolling
  • Adding custom easing functions
  • Creating pause/slow on hover effects

Core Animation

The main magic of this component is the useAnimationFrame hook from Motion, which executes our anim code on every frame. Here's how it works:

  1. We create motion values (using useMotionValue) to track the x or y position:

Motion values

1const baseX = useMotionValue(0)
2const baseY = useMotionValue(0)
  1. We define a baseVelocity prop that determines the default speed and direction:

Base velocity

1// Convert baseVelocity to the correct direction
2 const actualBaseVelocity =
3 direction === "left" || direction === "up" ? -baseVelocity : baseVelocity
  1. On each animation frame inside the useAnimationFrame hook, we increment the position values, by adding that velocity to the current position:

Animation frame

1// Inside useAnimationFrame
2let moveBy = directionFactor.current * baseVelocity * (delta / 1000)
3
4if (isHorizontal) {
5 baseX.set(baseX.get() + moveBy)
6} else {
7 baseY.set(baseY.get() + moveBy)
8}
  1. Since we're constantly increasing/decreasing that value, at some point our elements would move out far away from the viewport. Therefore, we use the useTransform hook to convert that x/y value to a percentage, and wrapping it between 0 and -100. With this, we essentially force our elements to always move from 0 to -100. Once they reach -100, they will start their journey from 0% again.

Transformation

1const x = useTransform(baseX, (v) => {
2 // wrap it between 0 and -100
3 const wrappedValue = wrap(0, -100, v)
4 // Apply easing if provided, otherwise use linear
5 return `${easing ? easing(wrappedValue / -100) * -100 : wrappedValue}%`
6})
  1. The wrap helper function ensures values stay between 0 and -100:

Wrapping

1const wrap = (min: number, max: number, value: number): number => {
2 const range = max - min
3 return ((((value - min) % range) + range) % range) + min
4}

This example demonstrates the basic mechanism:

ITEM 01fancy
ITEM 02fancy

Preventing "Jumps" With Repetition

As you can see above, elements eventually leave the container and jump back to the beginning when they reach -100%. This creates a visible "jump" in the animation.

We can solve this by using the repeat prop to duplicate all child elements multiple times inside the component:

Repeat example

1{
2 Array.from({ length: repeat }, (_, i) => i).map((i) => (
3 <motion.div
4 key={i}
5 className={cn(
6 "shrink-0",
7 isHorizontal && "flex",
8 draggable && grabCursor && "cursor-grab"
9 )}
10 style={isHorizontal ? { x } : { y }}
11 aria-hidden={i > 0}
12 >
13 {children}
14 </motion.div>
15 ))
16}

By default, the repeat value is 3, which means your content is duplicated three times. With enough repetitions, new elements enter the visible area before existing ones leave, creating an illusion of continuous animation. Try increasing the repeat value in the demo above to see how it eliminates the jumpiness.

Features


The marquee's final velocity and behavior are determined by combining several factors that can be enabled through props:

Slow Down On Hover

When slowdownOnHover is set to true, the component tracks hover state and applies a slowdown factor:

Slow down on hover

1// Track hover state
2const isHovered = useRef(false)
3const hoverFactorValue = useMotionValue(1)
4const smoothHoverFactor = useSpring(hoverFactorValue, slowDownSpringConfig)
5
6// In component JSX
7<motion.div
8 onHoverStart={() => (isHovered.current = true)}
9 onHoverEnd={() => (isHovered.current = false)}
10 // ...other props
11>
12 {/* ... */}
13</motion.div>
14
15// In animation frame
16if (isHovered.current) {
17 hoverFactorValue.set(slowdownOnHover ? slowDownFactor : 1)
18} else {
19 hoverFactorValue.set(1)
20}
21
22// Apply the hover factor to movement calculation
23let moveBy = directionFactor.current *
24 actualBaseVelocity *
25 (delta / 1000) *
26 smoothHoverFactor.get()

Key props for this feature:

  • slowDownFactor controls how much to slow down (default: 0.3 or 30% of original speed)
  • smoothHoverFactor uses spring physics for smooth transitions between speeds. This ensures that the velocity change is not happening instantly, but with a smooth animation. For this, we use the useSpring hook from Motion.
  • slowDownSpringConfig lets you customize the spring animation parameters. Please refer to the Motion documentation for more details.

Scroll-Based Velocity

When useScrollVelocity is enabled, the component tracks scroll velocity and uses it to influence the final velocity of the marquee:

Scroll velocity

1const { scrollY } = useScroll({
2 container: (scrollContainer as RefObject<HTMLDivElement>) || innerContainer.current,
3})
4const scrollVelocity = useVelocity(scrollY)
5const smoothVelocity = useSpring(scrollVelocity, scrollSpringConfig)
6
7// Transform scroll velocity into a factor for marquee speed
8const velocityFactor = useTransform(
9 useScrollVelocity ? smoothVelocity : defaultVelocity,
10 [0, 1000],
11 [0, 5],
12 { clamp: false }
13)
14
15// In animation frame
16// Adjust movement based on scroll velocity
17moveBy += directionFactor.current * moveBy * velocityFactor.get()
18
19// Change direction based on scroll if enabled
20if (scrollAwareDirection && !isDragging.current) {
21 if (velocityFactor.get() < 0) {
22 directionFactor.current = -1
23 } else if (velocityFactor.get() > 0) {
24 directionFactor.current = 1
25 }
26}

This creates an interactive effect where:

  • Scrolling adds to the marquee's velocity
  • If scrollAwareDirection is enabled, the scroll direction can reverse the marquee direction
  • Similar to the hover, we interpolate between the current and scroll velocity by using Spring physics with the useSpring hook from Motion. You can customize the spring animation parameters using the scrollSpringConfig prop.

Custom Easing Functions

The easing prop allows you to transform the linear animation with custom easing curves:

Custom easing

1const x = useTransform(baseX, (v) => {
2 // Apply easing if provided, otherwise use linear
3 const wrappedValue = wrap(0, -100, v)
4 return `${easing ? easing(wrappedValue / -100) * -100 : wrappedValue}%`
5})

The easing function receives a normalized value between 0 and 1 and should return a transformed value. You need to provide an actual function here, not defined keyframes.

You can find ready-to-use easing functions at easings.net.

Image 1
Image 2
Image 3
Image 4
Image 5
Image 5
Image 6
Image 7
Image 8
Image 9
Image 10
Image 11
Image 12
Image 13
Image 14

Draggable Marquee

The marquee can also be dragged. It uses pointer events for tracking the cursor position and applying the drag velocity:

Dragging

1// State for tracking dragging
2const isDragging = useRef(false)
3const dragVelocity = useRef(0)
4const lastPointerPosition = useRef({ x: 0, y: 0 })
5
6const handlePointerDown = (e: React.PointerEvent) => {
7 if (!draggable) return
8 // Capture pointer events
9 (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId)
10
11 if (grabCursor) {
12 (e.currentTarget as HTMLElement).style.cursor = "grabbing"
13 }
14
15 isDragging.current = true
16 lastPointerPosition.current = { x: e.clientX, y: e.clientY }
17
18 // Pause automatic animation
19 dragVelocity.current = 0
20}
21
22const handlePointerMove = (e: React.PointerEvent) => {
23 if (!draggable || !isDragging.current) return
24
25 const currentPosition = { x: e.clientX, y: e.clientY }
26
27 // Calculate movement delta
28 const deltaX = currentPosition.x - lastPointerPosition.current.x
29 const deltaY = currentPosition.y - lastPointerPosition.current.y
30
31 // Support for angled dragging
32 const angleInRadians = (dragAngle * Math.PI) / 180
33 const directionX = Math.cos(angleInRadians)
34 const directionY = Math.sin(angleInRadians)
35
36 // Project movement along angle direction
37 const projectedDelta = deltaX * directionX + deltaY * directionY
38
39 // Set drag velocity
40 dragVelocity.current = projectedDelta * dragSensitivity
41
42 lastPointerPosition.current = currentPosition
43}

During animation frames, dragging takes precedence over other movement factors. Meaning, when the user is dragging, the marquee will move according to the drag velocity, and we ignore all other factors (such as the hover, scroll and the basic velocity).

Drag animation frame

1// Inside useAnimationFrame
2if (isDragging.current && draggable) {
3 if (isHorizontal) {
4 baseX.set(baseX.get() + dragVelocity.current)
5 } else {
6 baseY.set(baseY.get() + dragVelocity.current)
7 }
8
9 // Add decay to dragVelocity when not moving
10 dragVelocity.current *= 0.9
11
12 // Stop completely if velocity is very small
13 if (Math.abs(dragVelocity.current) < 0.01) {
14 dragVelocity.current = 0
15 }
16
17 return
18}

When the user stops dragging, velocity gradually decays back to the base velocity. You can customize the decay factor using the dragVelocityDecay prop.

Drag velocity decay

1// Gradually decay drag velocity back to zero
2if (!isDragging.current && Math.abs(dragVelocity.current) > 0.01) {
3 dragVelocity.current *= dragVelocityDecay
4} else if (!isDragging.current) {
5 dragVelocity.current = 0
6}

The component also supports changing direction based on drag movement:

Drag direction

1// Update direction based on drag direction
2if (dragAwareDirection && Math.abs(dragVelocity.current) > 0.1) {
3 // If dragging in negative direction, set directionFactor to -1
4 // If dragging in positive direction, set directionFactor to 1
5 directionFactor.current = Math.sign(dragVelocity.current)
6}

New Arrivals

Image 1
Image 2
Image 3
Image 4
Image 5
Image 6
Image 7
Image 8
Image 9
Image 10

Artwork credits: Artworks are from Cosmos. I couldn't track down the original artists.

3D Transforms


To make a 3d effect, you can apply 3D CSS transforms to the marquee container or its children. The following example shows how you can apply them on the container.

Weekly Mix

Loading album covers...

For angled marquees, you can also apply the dragAngle prop to change the direction of the drag movement. This is useful if you want to rotate the marquee e.g. by 45 degrees.

3D transforms

1// Convert dragAngle from degrees to radians
2const angleInRadians = (dragAngle * Math.PI) / 180
3
4// Calculate the projection of the movement along the angle direction
5const directionX = Math.cos(angleInRadians)
6const directionY = Math.sin(angleInRadians)
7
8// Project the movement onto the angle direction
9const projectedDelta = deltaX * directionX + deltaY * directionY

Resources


Props


PropTypeDefaultDescription

children*

ReactNode-The elements to be scrolled
classNamestring-Additional CSS classes for the container
direction"left" | "right" | "up" | "down"right

The direction of the marquee. Set to "left" or "right" to scroll from left to right, or "up" or "down" to scroll from top to bottom

baseVelocitynumber5The base velocity of the marquee in pixels per second
easing(value: number) => number-

The easing function for the animation

slowdownOnHoverbooleanfalseWhether to slow down the animation on hover
slowDownFactornumber0.3The factor to slow down the animation on hover
slowDownSpringConfigSpringOptions{ damping: 50, stiffness: 400 }

The spring config for the slow down animation

useScrollVelocitybooleanfalseWhether to use the scroll velocity to control the marquee speed
scrollAwareDirectionbooleanfalseWhether to adjust the direction based on the scroll direction
scrollSpringConfigSpringOptions{ damping: 50, stiffness: 400 }The spring config for the scroll velocity-based direction adjustment
scrollContainerRefObject<HTMLElement> | HTMLElement | null-The container to use for the scroll velocity. If not provided, the window will be used.
repeatnumber3The number of times to repeat the children
draggablebooleanfalseWhether to allow dragging of the marquee
dragSensitivitynumber0.2The sensitivity of the drag movement
dragVelocityDecaynumber0.96The decay of the drag velocity when released
dragAwareDirectionbooleanfalseWhether to adjust the direction based on the drag velocity
dragAnglenumber0The angle of the drag movement in degrees
grabCursorbooleanfalseWhether to change the cursor to grabbing when dragging