22 CSS Image Galleries06 / 22

CSS + JSMIT licensed

CSS Interactive Pointer Trail Gallery

A playful canvas where images bloom and float behind your cursor as you move across it, then gently drift and fade — the cursor-trail image effect creative portfolios and landing pages search for. CSS owns the animation; a tiny script spawns the trail.

Published Updated

Live Demo
Try it

The code

<section class="gl-20" aria-label="Interactive pointer trail gallery">
  <div class="gl-20__canvas" id="gl-20-canvas" role="img" aria-label="Move your cursor to leave a trail of images">
    <p class="gl-20__hint">move your cursor across the canvas</p>
  </div>
</section>
.gl-20,
.gl-20 *,
.gl-20 *::before,
.gl-20 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.gl-20 ::selection {
  background: oklch(0.72 0.18 320);
  color: #fff;
}

.gl-20 {
  font-family: 'Segoe UI',system-ui,sans-serif;
  width: 100%;
  min-height: 100vh;
  padding: 40px 20px;
  background: radial-gradient(120% 120% at 50% 30%,#171526,#08070f);
  display: flex;
  align-items: center;
  justify-content: center;
}

.gl-20__canvas {
  position: relative;
  width: min(860px,100%);
  height: min(520px,70vh);
  border-radius: 20px;
  overflow: hidden;
  cursor: crosshair;
  background: linear-gradient(135deg,rgba(255,255,255,.03),rgba(255,255,255,.01));
  border: 1px solid rgba(255,255,255,.08);
  touch-action: none;
}

.gl-20__hint {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  color: rgba(255,255,255,.35);
  font: 12px ui-monospace,Menlo,monospace;
  letter-spacing: .2em;
  text-transform: uppercase;
  pointer-events: none;
}

.gl-20__spark {
  position: absolute;
  width: 120px;
  height: 150px;
  margin: -75px 0 0 -60px;
  border-radius: 12px;
  object-fit: cover;
  pointer-events: none;
  will-change: transform,opacity;
  box-shadow: 0 12px 30px -12px rgba(0,0,0,.6);
  animation: gl-20-bloom 1.1s cubic-bezier(.2,.7,.2,1) forwards;
}

@keyframes gl-20-bloom {
  0% {
    opacity: 0;
    transform: scale(.4) rotate(-6deg);
  }

  18% {
    opacity: 1;
    transform: scale(1) rotate(0deg);
  }

  100% {
    opacity: 0;
    transform: scale(.86) translateY(40px) rotate(4deg);
  }
}

@media (prefers-reduced-motion: reduce) {
  .gl-20__spark {
    display: none;
  }
}
(function(){
  var canvas = document.getElementById('gl-20-canvas');
  if(!canvas) return;
  if(matchMedia('(hover:none)').matches || matchMedia('(prefers-reduced-motion:reduce)').matches) return;
  var pool = [1801,1803,1002,1601,1101,1301].map(function(n){
    return 'https://loremflickr.com/240/300/nature,art?lock=' + n;
  });
  var i = 0, last = 0;
  canvas.addEventListener('pointermove', function(e){
    var now = Date.now();
    if(now - last < 90) return;      // throttle spawn rate
    last = now;
    var r = canvas.getBoundingClientRect();
    var img = document.createElement('img');
    img.className = 'gl-20__spark';
    img.alt = '';
    img.setAttribute('aria-hidden','true');
    img.src = pool[i % pool.length]; i++;
    img.style.left = (e.clientX - r.left) + 'px';
    img.style.top  = (e.clientY - r.top) + 'px';
    img.addEventListener('animationend', function(){ img.remove(); });
    canvas.appendChild(img);
  });
})();
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Image Gallerie from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: CSS Interactive Pointer Trail Gallery
Source: https://codefronts.com/snippets/css-image-gallery/css-interactive-pointer-trail-gallery/

A playful canvas where images bloom and float behind your cursor as you move across it, then gently drift and fade — the cursor-trail image effect creative portfolios and landing pages search for. CSS owns the animation; a tiny script spawns the trail.
## HTML
```html
<section class="gl-20" aria-label="Interactive pointer trail gallery">
  <div class="gl-20__canvas" id="gl-20-canvas" role="img" aria-label="Move your cursor to leave a trail of images">
    <p class="gl-20__hint">move your cursor across the canvas</p>
  </div>
</section>
```
## CSS
```css
.gl-20,
.gl-20 *,
.gl-20 *::before,
.gl-20 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.gl-20 ::selection {
  background: oklch(0.72 0.18 320);
  color: #fff;
}

.gl-20 {
  font-family: 'Segoe UI',system-ui,sans-serif;
  width: 100%;
  min-height: 100vh;
  padding: 40px 20px;
  background: radial-gradient(120% 120% at 50% 30%,#171526,#08070f);
  display: flex;
  align-items: center;
  justify-content: center;
}

.gl-20__canvas {
  position: relative;
  width: min(860px,100%);
  height: min(520px,70vh);
  border-radius: 20px;
  overflow: hidden;
  cursor: crosshair;
  background: linear-gradient(135deg,rgba(255,255,255,.03),rgba(255,255,255,.01));
  border: 1px solid rgba(255,255,255,.08);
  touch-action: none;
}

.gl-20__hint {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  color: rgba(255,255,255,.35);
  font: 12px ui-monospace,Menlo,monospace;
  letter-spacing: .2em;
  text-transform: uppercase;
  pointer-events: none;
}

.gl-20__spark {
  position: absolute;
  width: 120px;
  height: 150px;
  margin: -75px 0 0 -60px;
  border-radius: 12px;
  object-fit: cover;
  pointer-events: none;
  will-change: transform,opacity;
  box-shadow: 0 12px 30px -12px rgba(0,0,0,.6);
  animation: gl-20-bloom 1.1s cubic-bezier(.2,.7,.2,1) forwards;
}

@keyframes gl-20-bloom {
  0% {
    opacity: 0;
    transform: scale(.4) rotate(-6deg);
  }

  18% {
    opacity: 1;
    transform: scale(1) rotate(0deg);
  }

  100% {
    opacity: 0;
    transform: scale(.86) translateY(40px) rotate(4deg);
  }
}

@media (prefers-reduced-motion: reduce) {
  .gl-20__spark {
    display: none;
  }
}
```

## JavaScript
```js
(function(){
  var canvas = document.getElementById('gl-20-canvas');
  if(!canvas) return;
  if(matchMedia('(hover:none)').matches || matchMedia('(prefers-reduced-motion:reduce)').matches) return;
  var pool = [1801,1803,1002,1601,1101,1301].map(function(n){
    return 'https://loremflickr.com/240/300/nature,art?lock=' + n;
  });
  var i = 0, last = 0;
  canvas.addEventListener('pointermove', function(e){
    var now = Date.now();
    if(now - last < 90) return;      // throttle spawn rate
    last = now;
    var r = canvas.getBoundingClientRect();
    var img = document.createElement('img');
    img.className = 'gl-20__spark';
    img.alt = '';
    img.setAttribute('aria-hidden','true');
    img.src = pool[i % pool.length]; i++;
    img.style.left = (e.clientX - r.left) + 'px';
    img.style.top  = (e.clientY - r.top) + 'px';
    img.addEventListener('animationend', function(){ img.remove(); });
    canvas.appendChild(img);
  });
})();
```

How this works

A small pointer handler throttles cursor moves and drops an <img> at the pointer position, cycling through a pool of photos. Each spawned image runs a single CSS @keyframes that scales it up, drifts it slightly and fades it out, then a one-shot animationend listener removes it — so the DOM stays clean and the trail is self-limiting.

All motion (the bloom, drift and fade) is defined in CSS on transform/opacity; JS only decides where and when to spawn. On touch or under reduced-motion the effect no-ops and the canvas shows a static hint.

Make it yours

  • Change the trail imagery by editing the URL pool in the script.
  • Adjust density with the spawn throttle interval (lower = denser trail).
  • Restyle the bloom keyframe — spin, blur-in, or a bigger drift.
  • Cap simultaneous images if you want a sparser, calmer effect.

Gotchas — read before shipping

  • Throttle spawning and self-remove each node on animationend, or the DOM balloons and INP suffers.
  • No-op on hover:none (touch) — a cursor trail makes no sense without a cursor.
  • Honour prefers-reduced-motion; a busy spawning trail is a motion-sensitivity trigger.

Browser support

ChromeSafariFirefoxEdge
111+15.4+113+111+

Floor set by oklch() as this demo is written. The core technique itself goes back further — Chrome 43+.

CSS keyframes drive the trail; a small JS spawner places nodes. Degrades to a static panel with no JS or under reduced-motion.

Techniques used in this demo

Search CodeFronts

Loading…