Simple Marquee
Simple Marquee is a React and Tailwind CSS marquee component from Fancy Components, licensed MIT. Copy it and paste it into your project.
Fancy Components
MIT
marquee
What it is
Simple Marquee is a React and Tailwind CSS marquee component from Fancy Components, licensed MIT. Copy it and paste it into your project.
How to use it
Open it in the catalogue, press copy, and paste it into your project. The prompt you copy carries the code and the rules that tell your agent to reproduce it without changing anything.
Five free copies a month. No card.
Needs
npm i motion
Licence
This component comes from Fancy Components and is redistributed under the MIT licence, which permits commercial use. The copyright notice travels with the code.
Simple Marquee
import { RefObject, useRef } from "react"
import {
motion,
SpringOptions,
useAnimationFrame,
useMotionValue,
useScroll,
useSpring,
useTransform,
useVelocity,
} from "motion/react"
import { cn } from "@/lib/utils"
// Custom wrap function
const wrap = (min: number, max: number, value: number): number => {
const range = max - min
return ((((value - min) % range) + range) % range) + min
}
interface SimpleMarqueeProps {
children: React.ReactNode // The elements to be scrolled
className?: string // Additional CSS classes for the container
direction?: "left" | "right" | "up" | "down" // The direction of the marquee
baseVelocity?: number // The base velocity of the marquee in pixels per second
easing?: (value: number) => number // The easing function for the animation
slowdownOnHover?: boolean // Whether to slow down the animation on hover
slowDownFactor?: number // The factor to slow down the animation on hover
slowDownSpringConfig?: SpringOptions // The spring config for the slow down animation
useScrollVelocity?: boolean // Whether to use the scroll velocity to control the marquee speed
scrollAwareDirection?: boolean // Whether to adjust the direction based on the scroll direction
scrollSpringConfig?: SpringOptions // The spring config for the scroll velocity-based direction adjustment
scrollContainer?: RefObject<HTMLElement | null> | HTMLElement | null // The container to use for the scroll velocity
repeat?: number // The number of times to repeat the children.
draggable?: boolean // Whether to allow dragging of the marquee
dragSensitivity?: number // The sensitivity of the drag movement
dragVelocityDecay?: number // The decay of the drag velocity. This means how fast the velocity will gradually reduce to baseVelocity when we release the drag
dragAwareDirection?: boolean // Whether to adjust the direction based on the drag velocity
dragAngle?: number // The angle of the drag movement in degrees. This is useful if you eg. rotating your marquee by 45 degrees
grabCursor?: boolean // Whether to change the cursor to grabbing when dragging
}
const SimpleMarquee = ({
children,
className,
direction = "right",
baseVelocity = 5,
slowdownOnHover = false,
slowDownFactor = 0.3,
slowDownSpringConfig = { damping: 50, stiffness: 400 },
useScrollVelocity = false,
scrollAwareDirection = false,
scrollSpringConfig = { damping: 50, stiffness: 400 },
scrollContainer,
repeat = 3,
draggable = false,
dragSensitivity = 0.2,
dragVelocityDecay = 0.96,
dragAwareDirection = false,
dragAngle = 0,
grabCursor = false,
easing,
}: SimpleMarqueeProps) => {
const baseX = useMotionValue(0)
const baseY = useMotionValue(0)
const { scrollY } = useScroll({
...(scrollContainer && {
container: scrollContainer as RefObject<HTMLDivElement>,
}),
})
const scrollVelocity = useVelocity(scrollY)
const smoothVelocity = useSpring(scrollVelocity, scrollSpringConfig)
const hoverFactorValue = useMotionValue(1)
const defaultVelocity = useMotionValue(1)
// Track if user is currently dragging
const isDragging = useRef(false)
// Store drag velocity
const dragVelocity = useRef(0)
const smoothHoverFactor = useSpring(hoverFactorValue, slowDownSpringConfig)
// Transform scroll velocity into a factor that affects marquee speed
const velocityFactor = useTransform(
useScrollVelocity ? smoothVelocity : defaultVelocity,
[0, 1000],
[0, 5],
{
clamp: false,
}
)
// Determine if movement is horizontal or vertical.
const isHorizontal = direction === "left" || direction === "right"
// Convert baseVelocity to the correct direction
const actualBaseVelocity =
direction === "left" || direction === "up" ? -baseVelocity : baseVelocity
// Reference to track if mouse is hovering
const isHovered = useRef(false)
// Direction factor for changing direction based on scroll or drag
const directionFactor = useRef(1)
// Transform baseX/baseY into a percentage for the transform
// The wrap function ensures the value stays between 0 and -100
const x = useTransform(baseX, (v) => {
// Apply easing if provided, otherwise use linear (v directly)
const wrappedValue = wrap(0, -100, v)
return `${easing ? easing(wrappedValue / -100) * -100 : wrappedValue}%`
})
const y = useTransform(baseY, (v) => {
// Apply easing if provided, otherwise use linear (v directly)
const wrappedValue = wrap(0, -100, v)
return `${easing ? easing(wrappedValue / -100) * -100 : wrappedValue}%`
})
useAnimationFrame((t, delta) => {
if (isDragging.current && draggable) {
if (isHorizontal) {
baseX.set(baseX.get() + dragVelocity.current)
} else {
baseY.set(baseY.get() + dragVelocity.current)
}
// Add decay to dragVelocity when not moving
// This will gradually reduce the velocity to zero when the pointer isn't moving
dragVelocity.current *= 0.9
// Stop completely if velocity is very small
if (Math.abs(dragVelocity.current) < 0.01) {
dragVelocity.current = 0
}
return
}
// Update hover factor
if (isHovered.current) {
hoverFactorValue.set(slowdownOnHover ? slowDownFactor : 1)
} else {
hoverFactorValue.set(1)
}
// Calculate regular movement
let moveBy =
directionFactor.current *
actualBaseVelocity *
(delta / 1000) *
smoothHoverFactor.get()
// Adjust movement based on scroll velocity if scrollAwareDirection is enabled
if (scrollAwareDirection && !isDragging.current) {
if (velocityFactor.get() < 0) {
directionFactor.current = -1
} else if (velocityFactor.get() > 0) {
directionFactor.current = 1
}
}
moveBy += directionFactor.current * moveBy * velocityFactor.get()
if (draggable) {
moveBy += dragVelocity.current
// Update direction based on drag direction if dragAwareDirection is true
if (dragAwareDirection && Math.abs(dragVelocity.current) > 0.1) {
// If dragging in negative direction, set directionFactor to -1
// If dragging in positive direction, set directionFactor to 1
directionFactor.current = Math.sign(dragVelocity.current)
}
// Gradually decay drag velocity back to zero
if (!isDragging.current && Math.abs(dragVelocity.current) > 0.01) {
dragVelocity.current *= dragVelocityDecay
} else if (!isDragging.current) {
dragVelocity.current = 0
}
}
if (isHorizontal) {
baseX.set(baseX.get() + moveBy)
} else {
baseY.set(baseY.get() + moveBy)
}
})
const lastPointerPosition = useRef({ x: 0, y: 0 })
const handlePointerDown = (e: React.PointerEvent) => {
if (!draggable)
return // Capture the pointer to receive events even when pointer moves outside
;(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId)
if (grabCursor) {
;(e.currentTarget as HTMLElement).style.cursor = "grabbing"
}
isDragging.current = true
lastPointerPosition.current = { x: e.clientX, y: e.clientY }
// Pause automatic animation by setting velocity to 0
dragVelocity.current = 0
}
const handlePointerMove = (e: React.PointerEvent) => {
if (!draggable || !isDragging.current) return
const currentPosition = { x: e.clientX, y: e.clientY }
// Calculate delta from last position
const deltaX = currentPosition.x - lastPointerPosition.current.x
const deltaY = currentPosition.y - lastPointerPosition.current.y
// Convert dragAngle from degrees to radians
const angleInRadians = (dragAngle * Math.PI) / 180
// Calculate the projection of the movement along the angle direction
// Using the dot product of the movement vector and the direction vector
const directionX = Math.cos(angleInRadians)
const directionY = Math.sin(angleInRadians)
// Project the movement onto the angle direction
const projectedDelta = deltaX * directionX + deltaY * directionY
// Update drag velocity based on the projected movement
dragVelocity.current = projectedDelta * dragSensitivity
// Update last position
lastPointerPosition.current = currentPosition
}
const handlePointerUp = (e: React.PointerEvent) => {
if (!draggable) return // Release pointer capture
;(e.currentTarget as HTMLElement).releasePointerCapture(e.pointerId)
isDragging.current = false
}
return (
<motion.div
className={cn("flex", isHorizontal ? "flex-row" : "flex-col", className)}
onHoverStart={() => (isHovered.current = true)}
onHoverEnd={() => (isHovered.current = false)}
onPointerDown={handlePointerDown}
onPointerMove={handlePointerMove}
onPointerUp={handlePointerUp}
onPointerCancel={handlePointerUp}
>
{Array.from({ length: repeat }, (_, i) => i).map((i) => (
<motion.div
key={i}
className={cn(
"shrink-0",
isHorizontal && "flex",
draggable && grabCursor && "cursor-grab"
)}
style={isHorizontal ? { x } : { y }}
aria-hidden={i > 0}
>
{children}
</motion.div>
))}
</motion.div>
)
}
export default SimpleMarquee