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
-
Tracking Position: The component continuously monitors the mouse or touch position.
-
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 )
-
Controlling Responsiveness (
intensity
): Theintensity
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
-
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:
- 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 therepeatChildren
prop to a number greater than 1 to duplicate the children internally and avoid immediate repetition. - Making Items Visible: Trail items are initially hidden (
display: none
). When triggered, the next item is made visible (display: block
) and starts its animation. - Animating Position: The core movement animation uses the
animate
function fromMotion
. Each triggered item animates itsx
andy
coordinates towards the continuously updatingcachedMousePos
(calculated usinglerp
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 inkeyframes
. Thetimes
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
to0.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 theImageTrail
component. Be cautious when applying customclassName
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 theclassName
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
Prop | Type | Default | Description |
---|---|---|---|
children* | React.ReactNode | - | The content to be displayed |
threshold | number | 100 | How much distance in pixels the mouse has to travel to trigger an element to appear |
as | ElementType | div | HTML Tag |
intensity | number | 0.3 | The 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 |
keyframes | DOMKeyframesDefinition | - | Animation Keyframes for defining the animation sequence. Example: { scale: [0, 1, 1, 0] } |
keyframesOptions | AnimationOptions | - | 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 |
repeatChildren | number | 3 | The number of times the children will be repeated |
baseZIndex | number | 0 | The 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 |
className | string | - | Additional CSS class names |
Image Trail Item
Prop | Type | Default | Description |
---|---|---|---|
children* | React.ReactNode | - | The content to be displayed |
as | ElementType | div | HTML Tag |
className | string | - | Additional CSS class names |