Atelier UI®

Read the docsGithub
Docs 0.7.0

Getting started

  • Browse Catalog
  • Installation
  • How to contribute
  • Code of conduct

Components (19)

  • Dither Flow
    pro
  • Glowing Fog
    pro
  • Halftone Glow
    pro
  • Fluid Distortion
    new
  • Image Trail
    new
  • Liquid Image
    new
  • Magnetic Dot Grid
    new
  • Pixel Trail
    new
  • Pixelated Text
    new
  • Simple Scramble
    new
  • Text Bounce
    new
  • Text Fluid
    pro
  • Text Roll
    new
  • Curve Image
    new
  • Elastic Stick
    pro
  • Infinite Gallery
    new
  • Infinite Parallax
    new
  • Infinite Zoom
    new
  • Scattered Scroll
    new
Atelier UI 0.7.0 ©2026
Star on githubBuy me a coffeellms.txt
  1. Docs
  2. /
  3. Components
  4. /
  5. Image Trail

Image Trail

Cursor trail effect that spawns images along it's path.

Motion
Tailwind CSS
https://atelier-ui.com/image-trail

Settings

drift-amount
36
spawn-distance
76
remove-delay
1.0
See the documentation below for more options.

CLI Install

npx atelier-ui add image-trail

Manual Install

npm install motion
image-trail.tsx
import { delay, wrap } from "motion"
import {
    AnimatePresence,
    motion,
    useMotionValue,
    useMotionValueEvent,
    useTransform,
} from "motion/react"
import { useEffect, useRef, useState } from "react"

type Items<T> = {
    id: number
    x: number
    y: number
    driftX: number
    driftY: number
    rotate: number
    data: T
}

export type PropsMouseTrail<T> = {
    data: T[]
    renderItems: (item: T) => React.ReactNode
    removeDelay?: number
    driftAmount?: number
    spawnDistance?: number
}

export function ImageTrail<T>({
    data,
    renderItems,
    removeDelay = 1.0,
    driftAmount = 36,
    spawnDistance = 76,
}: PropsMouseTrail<T>) {
    const [items, setItems] = useState<Items<T>[]>([])
    const sum = useRef(0)
    const itemIndex = useRef(0)
    const idCounter = useRef(0)
    const pointerX = useMotionValue(0)
    const pointerY = useMotionValue(0)

    const distanceInPixels = useTransform(() => {
        const mouseX = pointerX.get()
        const mouseY = pointerY.get()
        const dx = mouseX - (pointerX.getPrevious() ?? mouseX)
        const dy = mouseY - (pointerY.getPrevious() ?? mouseY)

        return Math.sqrt(dx * dx + dy * dy)
    })

    useMotionValueEvent(distanceInPixels, "change", (latest) => {
        sum.current += latest
        if (sum.current >= spawnDistance) {
            const mouseX = pointerX.get()
            const mouseY = pointerY.get()

            const prevMouseX = pointerX.getPrevious() ?? mouseX
            const prevMouseY = pointerY.getPrevious() ?? mouseY

            const dx = mouseX - prevMouseX
            const dy = mouseY - prevMouseY

            const dist = Math.sqrt(dx * dx + dy * dy)

            const nx = dx / dist
            const ny = dy / dist
            const angle = Math.atan2(ny, nx) * (180 / Math.PI)
            const item = {
                id: idCounter.current++,
                x: mouseX,
                y: mouseY,
                driftX: nx * driftAmount + (Math.random() - 0.5) * driftAmount * 0.5,
                driftY: ny * driftAmount + (Math.random() - 0.5) * driftAmount * 0.5,
                rotate: angle * 0.15,
                data: data[itemIndex.current],
            }

            setItems((prev) => [...prev, item])

            itemIndex.current = wrap(0, data.length, itemIndex.current + 1)

            delay(() => {
                setItems((prev) => prev.filter((i) => i.id !== item.id))
            }, removeDelay)

            sum.current = 0
        }
    })

    useEffect(() => {
        const handlePointerMove = (event: PointerEvent) => {
            const e = event as PointerEvent
            pointerX.set(e.clientX)
            pointerY.set(e.clientY)
        }

        window.addEventListener("pointermove", handlePointerMove)
        return () => window.removeEventListener("pointermove", handlePointerMove)
    }, [pointerX, pointerY])

    return (
        <AnimatePresence>
            {items.map((item) => (
                <motion.div
                    key={item.id}
                    className="pointer-events-none"
                    style={{
                        position: "fixed",
                        left: item.x,
                        top: item.y,
                        translate: "-50% -50%",
                    }}
                    initial={{ scale: 0, x: 0, y: 0, rotate: item.rotate }}
                    animate={{
                        scale: 1,
                        x: item.driftX,
                        y: item.driftY,
                        rotate: item.rotate,
                    }}
                    exit={{ scale: 0, opacity: 0 }}
                    transition={{
                        scale: { type: "spring", stiffness: 260, damping: 20, mass: 1 },
                        x: { type: "spring", stiffness: 60, damping: 18, mass: 0.8 },
                        y: { type: "spring", stiffness: 60, damping: 18, mass: 0.8 },
                        rotate: { type: "spring", stiffness: 60, damping: 18, mass: 0.8 },
                    }}
                >
                    {renderItems(item.data)}
                </motion.div>
            ))}
        </AnimatePresence>
    )
}

API

NameTypeDefaultDescription
dataT[]—Array of items.
renderItem(item: T) => ReactNode—Render function called for each items.
spawnDistancenumber76Distance in pixels before spawning an item.
driftAmountnumber36How far each item drifts from its spawn point in px.
removeDelaynumber1.0Seconds before a spawned item is removed.

Credits

Motion
React animation library.

  • CLI Install
  • Manual Install
  • API
  • Credits
Star on githubBuy me a coffeellms.txt