Atelier UI®

DocsCatalogShader StudioPricingGithub
Docs 1.0.0

tools

  • Browse Catalog
  • Shader Studio
    pro
  • Collage
    new

Documentation

  • How it works
  • License
  • MCP

Page Transition (04)

  • Clip Transition
  • Stripe Transition
  • Pixel Transition
  • Band Transition

Components (36)

  • Orbit Gallery
  • Sphere Gallery
  • Spiral Gallery
  • Glowing Fog
  • Gradient Flow
  • Halftone Glow
  • Scattered Grid
  • Tag Cloud
  • Edge Bounce
  • Fluid Distortion
  • Image Trail
  • Lens Media
  • Liquid Media
  • Magnetic Dot Grid
  • Pixel Media
  • Pixel Trail
  • Dither Cursor
  • Hover Burst
  • Image Bloom
  • Curve Media
  • Infinite Gallery
  • Infinite Parallax
  • Infinite Zoom
  • Pixel Scroll
  • Scattered Scroll
  • Elastic Stick
  • Letter Swarm
  • Magnify Trail
  • Stacking Grid
  • Wavy Scroll
  • Pixelated Text
  • Text Bounce
  • Text Fluid
  • Text Scramble
  • Falling Text
  • Text Roll

Foundation Blocks (07)

  • Smooth Scroll
  • Text Split
  • WebGL Image
  • WebGL Provider
  • WebGL Scene
  • WebGL Text
  • WebGL Video
Atelier UI 1.0.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 its path.

https://atelier-ui.com/image-trail
drift-amount
36
spawn-distance
76
remove-delay
1.0
drift-amount
36
spawn-distance
76
remove-delay
1.0
Prompt
Add Atelier's Image Trail to my app.

If there is no components.json, run: npx shadcn@latest init -d
Then run: npx shadcn@latest add @atelier/image-trail
That writes the atelier-ui skill under .agents/skills and .claude/skills. Follow it.

Props: driftAmount={36} spawnDistance={76} removeDelay={1}

Just into your favorite coding agent and let Atelier do its magic. Live props are included.


This command will install all the dependencies this component uses.

npx shadcn@latest add @atelier/image-trail

Install the dependencies first, then feel free to copy the files into your project as you see fit.

npm install motion
image-trail.tsx
"use client"

import { delay, wrap } from "motion"
import {
    AnimatePresence,
    motion,
    useMotionValue,
    useMotionValueEvent,
    useTransform,
} from "motion/react"
import { Children, isValidElement, type ReactNode, useEffect, useRef, useState } from "react"

type TrailItem = {
    id: number
    x: number
    y: number
    driftX: number
    driftY: number
    rotate: number
    node: ReactNode
}

export type ImageTrailProps = {
    children: ReactNode
    removeDelay?: number
    driftAmount?: number
    spawnDistance?: number
}

export function ImageTrail({
    children,
    removeDelay = 1.0,
    driftAmount = 36,
    spawnDistance = 76,
}: ImageTrailProps) {
    const childrenArray = Children.toArray(children).filter(isValidElement)
    const [items, setItems] = useState<TrailItem[]>([])
    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: TrailItem = {
                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,
                node: childrenArray[itemIndex.current],
            }

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

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

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

            sum.current = 0
        }
    })

    useEffect(() => {
        const handlePointerMove = (event: PointerEvent) => {
            pointerX.set(event.clientX)
            pointerY.set(event.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 },
                    }}
                >
                    {item.node}
                </motion.div>
            ))}
        </AnimatePresence>
    )
}

Pass the trail items as children. They spawn along the cursor path.

const IMAGES = [
    "https://picsum.photos/seed/atelier-1/1200/800",
    "https://picsum.photos/seed/atelier-2/1200/800",
    "https://picsum.photos/seed/atelier-3/1200/800",
]

<ImageTrail>
    {IMAGES.map((src) => (
        <img key={src} src={src} className="w-32 h-32 object-cover rounded-xl" alt="" />
    ))}
</ImageTrail>

NameTypeDefaultDescription
childrenReactNode—Items to cycle through as the cursor moves. Required.
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.

Motion
React animation library.

Star on githubBuy me a coffeellms.txt