---
title: "How to Create a Cursor Image Trail Effect in Framer"
description: "Build a cursor image trail effect in Framer step by step — native limits, a working code override, and the fastest drop-in option."
canonical_url: "https://framerhub.io/blog/mouse-image-trail-framer"
last_updated: "2027-01-06T00:00:00.000Z"
---

You've seen this effect on agency and portfolio sites: move your cursor across a section and a trail of images blooms and fades behind it. It's one of the highest "wow factor per second of dev time" effects in modern web design — when it's built right. When it's built wrong, it stutters, lags, or spawns hundreds of unused DOM elements. Here's how to build it properly in Framer, plus the fastest way to skip straight to a polished version.

It's also one of the more misunderstood effects to implement, because the naive approach — creating a new `<img>` element on every mouse movement and letting it fade out with CSS — works fine in a five-second demo and falls apart the moment someone waves their cursor around for thirty seconds. By then you've created hundreds of orphaned DOM nodes, and the browser is doing real work cleaning them up. The fix isn't complicated, but it does require thinking about the effect as a fixed-size object pool instead of a spawn-and-forget animation, which is the core idea this guide walks through.

## Why This Effect Converts Attention Into Time-on-Page

Portfolio and agency sites live or die on a visitor's first ten seconds. A cursor trail gives people a reason to move their mouse around a hero section instead of scrolling past it immediately — and every extra second spent interacting with a page is a second spent absorbing the brand and the work samples inside the trail itself. Used well, the images in the trail aren't decorative; they're actual project thumbnails, so the "playing" is also "browsing." That's the difference between a gimmick and a genuinely useful piece of UI.

## What This Effect Actually Requires

A cursor image trail needs three things working together:

1. **Pointer tracking** — reading cursor (or touch) position in real time.
2. **A pool of images** — a fixed set of elements you reposition, not elements you create and destroy on every mouse movement.
3. **Staggered fade timing** — each image in the trail appears slightly after the last and fades out on its own delay, which is what makes the effect read as a "trail" instead of a single image snapping to the cursor.

Framer's native variants and interaction settings don't have a way to bind an element's position continuously to live pointer coordinates — that's a code-only capability, whether you write it yourself or drop in a pre-built component.

## Option 1: Build It With a Code Override

If you want to understand exactly how the effect works — or need a version tuned to a specific project — here's a working implementation using a fixed image pool.

```jsx
import { useRef, useCallback } from "react"
import { addPropertyControls, ControlType } from "framer"

const IMAGES = [
  "/img1.jpg",
  "/img2.jpg",
  "/img3.jpg",
  "/img4.jpg",
  "/img5.jpg",
]
const POOL_SIZE = 6
const MIN_DISTANCE = 60 // px the cursor must move before spawning the next image

export default function MouseTrail(props) {
  const containerRef = useRef(null)
  const poolRef = useRef([])
  const lastPos = useRef({ x: 0, y: 0 })
  const indexRef = useRef(0)

  const handleMouseMove = useCallback((e) => {
    const rect = containerRef.current.getBoundingClientRect()
    const x = e.clientX - rect.left
    const y = e.clientY - rect.top
    const dx = x - lastPos.current.x
    const dy = y - lastPos.current.y
    if (Math.sqrt(dx * dx + dy * dy) < MIN_DISTANCE) return

    lastPos.current = { x, y }
    const el = poolRef.current[indexRef.current % POOL_SIZE]
    if (!el) return

    el.style.left = `${x}px`
    el.style.top = `${y}px`
    el.style.backgroundImage = `url(${IMAGES[indexRef.current % IMAGES.length]})`
    el.style.opacity = "1"
    el.style.transform = "translate(-50%, -50%) scale(1)"

    // fade out on its own timer, independent of the next spawn
    clearTimeout(el.dataset.timeoutId)
    const t = setTimeout(() => {
      el.style.opacity = "0"
      el.style.transform = "translate(-50%, -50%) scale(0.8)"
    }, 400)
    el.dataset.timeoutId = t

    indexRef.current++
  }, [])

  return (
    <div
      ref={containerRef}
      onMouseMove={handleMouseMove}
      style={{ position: "relative", width: "100%", height: "100%", overflow: "hidden" }}
    >
      {Array.from({ length: POOL_SIZE }).map((_, i) => (
        <div
          key={i}
          ref={(el) => (poolRef.current[i] = el)}
          style={{
            position: "absolute",
            width: 120,
            height: 120,
            borderRadius: 8,
            backgroundSize: "cover",
            backgroundPosition: "center",
            opacity: 0,
            pointerEvents: "none",
            transition: "opacity 0.4s ease, transform 0.4s ease",
          }}
        />
      ))}
    </div>
  )
}

addPropertyControls(MouseTrail, {})
```

Paste this as a code component (not an override on an existing layer — it needs its own container to track mouse position against) in a fresh `.tsx` file inside your Framer project, drop it into the section where you want the trail, and set its width and height to fill the container.

The `MIN_DISTANCE` check is the detail that separates a smooth trail from a laggy mess — without it, every single `mousemove` event tries to spawn an image, which is far more update calls than the effect actually needs to look good. Throttling by distance moved, not by time, keeps the trail responsive when you move fast and calm when you move slow.

## Option 2: Add a Live Preview Note

*Team: insert a short screen-recording GIF here showing the trail effect in action across a portfolio hero section, cursor moving in a loop.*

## Choosing the Right Images for Your Trail

The code and the tuning matter less than what's actually inside the trail. A few things that consistently separate an effect that feels premium from one that feels like a tech demo:

- **Use real content, not stock filler.** Project thumbnails, product shots, or portfolio pieces give the trail a reason to exist beyond decoration. Random stock photography reads as an afterthought.
- **Keep a consistent aspect ratio across the image set.** Mixed ratios force you to either crop awkwardly or let the trail images jump in size, both of which undercut the smoothness of the motion.
- **Match the color grading across images.** A trail alternating between a warm-toned photo and a cool-toned photo flickers visually even though nothing about the animation changed — grade your trail set as a group, not individually.
- **Cap it at 5–10 distinct images.** Beyond that, visitors stop registering individual images and just see "a blur of pictures," which defeats the purpose if the images are meant to be a browsable preview of real work.

## Native Framer vs. Code Override vs. Component

<table>
<thead>
  <tr>
    <th>
      Approach
    </th>
    
    <th>
      Effort
    </th>
    
    <th>
      Customization
    </th>
    
    <th>
      Best for
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      Native Framer variants
    </td>
    
    <td>
      Not possible
    </td>
    
    <td>
      —
    </td>
    
    <td>
      N/A — this effect requires code
    </td>
  </tr>
  
  <tr>
    <td>
      Custom code override
    </td>
    
    <td>
      High (build + maintain)
    </td>
    
    <td>
      Full
    </td>
    
    <td>
      Teams with a developer who wants a bespoke, project-specific version
    </td>
  </tr>
  
  <tr>
    <td>
      MouseImageTrail / BouncingImageTrail component
    </td>
    
    <td>
      Low (drag, configure, done)
    </td>
    
    <td>
      Property-panel controls
    </td>
    
    <td>
      Everyone else who wants the effect shipped today
    </td>
  </tr>
</tbody>
</table>

## Accessibility and Reduced Motion

Cursor trails are pure decoration from an accessibility standpoint — no content depends on them — but they can still be visually overwhelming for visitors sensitive to motion. Wrap the effect so it checks `prefers-reduced-motion` and either disables the trail entirely or reduces it to a single, slower-fading image rather than a full multi-image cascade. This is a couple of lines added to the code override above (checking `window.matchMedia("(prefers-reduced-motion: reduce)").matches` before attaching the mousemove listener), and it's already handled for you if you use the pre-built component.

## Common Problems and Fixes

- **Images pop in instead of fading in.** Add a short `transition` on `opacity` and `transform` (as in the snippet above) rather than toggling `display: none/block`, which can't animate.
- **Trail feels laggy on fast mouse movement.** Lower `MIN_DISTANCE` slightly, or reduce the fade duration — a trail tuned for slow, deliberate movement will feel sluggish under a fast swipe.
- **Images stack up and never clear.** This usually means the fade timeout is getting cleared before it fires because a new event resets it too aggressively. Make sure each pool slot's timeout is independent, as shown above with `el.dataset.timeoutId`.
- **Effect triggers under text or buttons unintentionally.** Keep `pointerEvents: "none"` on the trail images (already in the snippet) so the trail never blocks clicks on content underneath it.

## Option 3: Drop In a Pre-Built Component

If the effect above is close to what you want but you'd rather not maintain the code, FramerHub's [MouseImageTrail component](/components/mouse-image-trail) does exactly this — cursor-following image trail with pool size, image list, fade timing, and easing all exposed as property panel controls, no code editing required. For a variation where images release and drift with their own physics instead of sticking to the cursor path, [BouncingImageTrail](/components/bouncing-image-trail) is the closer match. Both are part of FramerHub's [component library](/components), so if you're already using a few components on a project, adding one more doesn't mean managing another separate purchase or update workflow.

## Where This Effect Works (and Where It Doesn't)

Cursor trails are a section-level effect, not a whole-site effect. They work well in:

- **Portfolio hero sections**, where the trail images are project thumbnails
- **Agency "our work" sections**, previewing case studies as you hover across a list
- **Photography or gallery landing pages**, where the trail images are a curated set from the shoot

They work poorly stacked on top of dense text content (the images can obscure copy mid-read) or applied to more than one section per page — like most high-impact motion effects, a cursor trail loses its impact fast if it's everywhere.

If you're deciding where to place the one section that gets this treatment, the hero is almost always the right call. It's the section every visitor sees regardless of how far they scroll, and it's the section where a few extra seconds of engagement has the highest payoff — a visitor lingering in your hero to play with a cursor trail is a visitor who's about to read your headline twice instead of once.

## Performance Notes

Keep the image pool small (5–10 images) and pre-sized to the dimensions they'll display at — don't load full-resolution photography into a 120px trail image. Compress trail images more aggressively than you would a hero image; at the size and duration they're on screen, visual quality differences are invisible, but file size differences are not. If your trail images come from a CMS collection rather than a fixed list, our [Framer CMS gallery guide](/blog/framer-cms-gallery-guide) covers the broader patterns for pulling and optimizing CMS-driven imagery.

A second performance detail worth getting right: preload the images in the trail set before the effect can trigger. If the first image a visitor's cursor "spawns" is still loading from the network, they'll see a blank rounded rectangle flash in and fade out instead of a picture — which reads as broken rather than stylish. A simple `new Image().src = url` loop for each URL in your trail set on component mount is enough to warm the browser cache before the first mousemove event fires.

## Tuning the Feel: Speed, Size, and Density

Three variables control whether the effect feels premium or feels like a bug, and they interact with each other more than they first appear to:

- **Fade duration.** Around 350–500ms reads as intentional. Much shorter and the trail feels twitchy; much longer and images pile up faster than they can clear, especially during fast cursor movement.
- **Spawn distance (the MIN_DISTANCE threshold).** Smaller values create a denser, more continuous-feeling trail but cost more CPU per second of movement. Larger values feel sparser but stay smooth even on lower-powered devices. 50–80px is a reasonable starting range for most hero-section use cases.
- **Image size.** Larger trail images (150px+) read as bold and confident but obscure more of the content underneath. Smaller images (80–100px) feel more like a subtle cursor enhancement than a content preview. Match the size to how central the effect is to the section — a hero-defining trail can go bigger than a supporting one in a smaller card grid.

Adjust one variable at a time and test with your actual images and actual cursor movement speed rather than tuning against arbitrary numbers — the "right" values genuinely differ based on image content and section size.

## FAQ

**Can I build a cursor image trail in Framer without code?**
Not really — a true cursor-following image trail needs to track pointer position and stagger a pool of images in real time, which is outside what Framer's native variants and interaction settings can express. You need a code override or a pre-built component.

**Will a mouse image trail slow down my site?**
It can, if you spawn a new DOM element on every mousemove event. A well-built version reuses a fixed pool of image elements and repositions them instead of creating new ones, which keeps the effect smooth even on longer sessions.

**Does a cursor trail effect work on mobile?**
No — there's no cursor on a touch device, so the effect simply won't trigger. Design the section it lives in so it still looks complete without the trail, since a meaningful share of visitors will never see it.

**How many images should be in the trail?**
Most effective implementations use 5 to 10 images in the pool. Fewer than 5 feels sparse and stuttery; more than 10 gets visually noisy and adds unnecessary DOM overhead for images that are barely visible before they fade.

**What's the difference between MouseImageTrail and BouncingImageTrail?**
MouseImageTrail follows the cursor directly, spawning images along the path you move. BouncingImageTrail releases images that then animate with their own physics (drift, rotate, settle) instead of sticking to the cursor path — a slightly more playful, less literal version of the same idea.
