Docs
Image
Image Trail

Image Trail

A component that creates a trail effect on cursor/touch movement. Works also with videos, svgs, or any type of html elements.

by Khoa Phan

ALBUMS

Installation


npx shadcn@latest add "https://fancycomponents.dev/r/image-trail.json"

Introduction


This component is a fun mouse interaction effect. The idea is to follow the mouse and show a trail of random images. It's a kind of brutalist effect and there are various possibilities when it comes to showing and hiding the images.

Usage


Wrap each image with the ImageTrailItem component and wrap everything with the ImageTrail as a parent component.

<ImageTrail>
  <ImageTrailItem>...</ImageTrailItem>
  <ImageTrailItem>...</ImageTrailItem>
  <ImageTrailItem>...</ImageTrailItem>
</ImageTrail>

Understanding the component


The ImageTrail component creates its effect by tracking the mouse (or touch) position and animating a series of child elements (ImageTrailItem) to follow that movement with a configurable delay and visual style.

How the Trail Follows the Cursor

  1. Tracking Position: The component continuously monitors the mouse or touch position.

  2. Calculating Target Position (Linear Interpolation): Instead of instantly moving trail items to the current cursor position, it calculates a "target" position using linear interpolation (lerp). This smooths out the movement.

    const lerp = (a: number, b: number, n: number) => (1 - n) * a + n * b
    
    //...
    
    cachedMousePos.current.x = MathUtils.lerp(
      cachedMousePos.current.x || mousePos.x,
      mousePos.x,
      clampedIntensity
    )
    
    cachedMousePos.current.y = MathUtils.lerp(
      cachedMousePos.current.y || mousePos.y,
      mousePos.y,
      clampedIntensity
    )
    
  3. Controlling Responsiveness (intensity): The intensity prop (a value between 0 and 1) controls how quickly the calculated target position updates to match the actual cursor position.

    • Lower values (e.g., 0.1) result in a smoother, more delayed "momentum" effect, where the trail items lag behind the cursor.
    • Higher values (e.g., 0.8) make the trail more responsive but less smooth.
    • An intensity of 1 positions the trail items exactly at the cursor's current position.

    move your cursor

  4. Triggering Animation (threshold): An animation cycle for the next trail item is only triggered when the cursor moves a certain distance. This distance is calculated using the Pythagorean theorem (Math.hypot):

    const distance = (x1: number, y1: number, x2: number, y2: number) =>
      Math.hypot(x2 - x1, y2 - y1)
    

    The threshold prop (default: 100 pixels) defines this minimum distance. No new trail items are animated until the cursor has moved at least this far since the last item was triggered.

Animating the Trail Items

When the movement threshold is met:

  1. Cycling Through Items: The component activates the next available ImageTrailItem in the sequence. By default, it cycles through all children and repeats from the beginning. You can set the repeatChildren prop to a number greater than 1 to duplicate the children internally and avoid immediate repetition.
  2. Making Items Visible: Trail items are initially hidden (display: none). When triggered, the next item is made visible (display: block) and starts its animation.
  3. Animating Position: The core movement animation uses the animate function from Motion. Each triggered item animates its x and y coordinates towards the continuously updating cachedMousePos (calculated using lerp as described above).

Customizing the Visual Animation

Beyond the position animation, you can control how each ImageTrailItem visually appears and disappears using the keyframes and keyframesOptions props passed to the main ImageTrail component.

  • keyframes: Define the states of the animation (e.g., scale, opacity). You should define the initial state (how the element appears) and the final state (how it disappears).
  • keyframesOptions: Fine-tune the timing, duration, and easing for the properties defined in keyframes. The times array within options specifies the progress points (0 to 1) at which each keyframe state should be reached.

Example:

keyframes={{ scale: [0, 1, 1, 0], opacity: [0, 1, 1, 0] }}
keyframesOptions={{
  duration: 0.6, // Total duration for one item's animation
  scale: { times: [0, 0.1, 0.7, 1] }, // Scale keyframe timings
  opacity: { times: [0, 0.1, 0.7, 1] }, // Opacity keyframe timings
}}

In this example:

  • The item starts invisible (opacity: 0, scale: 0).
  • At 10% of the duration (0.06s), it quickly scales up and fades in (opacity: 1, scale: 1).
  • It remains fully visible until 70% of the duration (0.42s).
  • From 70% to 100% (0.42s to 0.6s), it scales down and fades out back to the initial state (opacity: 0, scale: 0).

There must be the same number of values in a times array as there are keyframes for that property. If times is omitted, the keyframes are spread evenly across the duration. You can read more about this in the Motion animate function documentation.

The component reuses DOM elements rather than creating new ones for each animation. It maintains a fixed set of ImageTrailItem elements that get recycled as needed. When an item needs to be triggered again, the component updates its position and restarts the animation without having to remove and remount elements in the DOM. For this recycling to work properly, make sure to define both initial and final states in the keyframes prop, as explained in the example above.

Z Index Stacking

The component automatically manages the z-index of the trail items to control their stacking order. You can customize this behavior using two props:

  • zIndexDirection: Controls whether newer or older items should appear on top

    • "new-on-top" (default): The most recently triggered item will have the highest z-index
    • "old-on-top": The oldest items stay on top, with new items appearing underneath
  • baseZIndex: Sets the starting z-index value (defaults to 0)

When a new item is triggered, the component maintains proper stacking by:

  • For "new-on-top": Setting the current item to the highest z-index and shifting all others down by 1
  • For "old-on-top": Setting the current item to the lowest z-index (baseZIndex) and shifting all others up by 1

Example with zIndexDirection="old-on-top":

move your cursor

That's it! :)

Examples


The component is not constrained to be used with images, you can wrap videos, svgs, or basically any HTML elements inside a ImageTrailItem.

move your cursor

Notes

  • The ImageTrailItem component assigns a default className of .image-trail-item to identify elements for animation within the ImageTrail component. Be cautious when applying custom className values with the same name (.image-trail-item) in your application, as this may cause conflicts or unintended behavior due to duplicate class selectors. To avoid issues, ensure custom classes are unique or use the className prop to extend styles without overriding the default .image-trail-item class.

  • When using the ImageTrail component, content is heavily animated. To prevent performance issues, avoid using overly large images or videos.

Props


Image Trail Wrapper

PropTypeDefaultDescription

children*

React.ReactNode-The content to be displayed
thresholdnumber100How much distance in pixels the mouse has to travel to trigger an element to appear
asElementTypedivHTML Tag
intensitynumber0.3The intensity for the momentum movement after showing the element. The value will be clamped greater than 0 and less than or equal to 1.0
keyframesDOMKeyframesDefinition-Animation Keyframes for defining the animation sequence. Example: { scale: [0, 1, 1, 0] }
keyframesOptionsAnimationOptions-Options for the animation/keyframes. Example: { duration: 1, times: [0, 0.1, 0.9, 1] }
trailElementAnimationKeyframes{ x?: AnimationOptions, y?: AnimationOptions }{ x: { duration: 1, type: "tween", ease: "easeOut" }, y: { duration: 1, type: "tween", ease: "easeOut" } }Animation keyframes for the x and y positions after showing the element. Describes how the element should try to arrive at the mouse position
repeatChildrennumber3The number of times the children will be repeated
baseZIndexnumber0The base zIndex for all elements
zIndexDirection"new-on-top" | "old-on-top""new-on-top"Controls stacking order behavior. "new-on-top": newer elements stack above older ones, "old-on-top": older elements stay visually on top
classNamestring-Additional CSS class names

Image Trail Item

PropTypeDefaultDescription

children*

React.ReactNode-The content to be displayed
asElementTypedivHTML Tag
classNamestring-Additional CSS class names