Image Trail

Images dropped along the pointer's path, spaced by distance travelled rather than by time.

Move the pointer across this panel.

Installation

npx shadcn@latest add @loomui/image-trail

Usage

import { ImageTrail } from "@/components/ui/image-trail"
<ImageTrail images={["/work/1.jpg", "/work/2.jpg", "/work/3.jpg"]}>
  <h1>Selected work</h1>
</ImageTrail>

Images are dropped by distance travelled, not on a timer. A slow drag across the panel leaves the same trail a fast flick does, and a pointer sitting still leaves nothing at all. On a timer, a paused pointer piles the whole set on one spot, which is the failure mode this effect is usually shipped with.

Anything you pass as children sits above the trail, so the effect goes behind a heading rather than in place of one. The panel is the hit area, so give it the size you want the effect to have.

Spacing and life

<ImageTrail images={IMAGES} distance={140} life={700} max={8} />

distance is how far the pointer must travel before the next image is dropped. Lower it and the trail becomes a dense ribbon. Raise it and you get a handful of deliberate marks. Around 90 is a trail. Around 200 is a breadcrumb.

life is how long one image takes to arrive, hold and go. It lands quickly and leaves slowly: an image that fades in at the same rate it fades out is never legible at any point in its life.

max caps how many are alive at once. Older images are dropped first, which keeps a fast sweep across a wide panel from putting eighty layers on screen.

Each image is removed on animationend rather than on a timer, so a tab that is backgrounded mid-sweep does not come back with a trail frozen across it.

Sizing the images

<ImageTrail images={IMAGES} imageClassName="h-40 w-32 rounded-2xl" />

className sizes the panel, imageClassName sizes what lands in it. Images are object-cover, so a mixed set of portrait and landscape sources still lands as one consistent shape.

Props

PropTypeDefaultDescription
imagesstring[]noneSources dropped in order, on a loop.
distancenumber90Pixels of travel between drops.
lifenumber900Milliseconds an image takes to arrive, hold and go.
maxnumber12Most images alive at once.
tiltnumber14Largest tilt either side of straight, in degrees.
imageClassNamestringnoneMerged onto each image. Size and radius here.
classNamestringnoneMerged onto the panel.

Any other <div> prop is forwarded.

Accessibility

Dropped images are decoration. They are aria-hidden, carry an empty alt, and are neither focusable nor draggable. The panel's real content is all a screen reader or a keyboard ever meets. Under prefers-reduced-motion nothing is dropped at all, and the panel is just a panel.