GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

Mouse Follow Effect CSS — 9 Cursor Chasers

A mouse follow effect css reads the pointer each frame and lets an element trail it by a beat — the core is one lerp line, no canvas, just transform.

Auto-plays · click a tile to jump to its section · all nine in one zip

The nine are not sorted by popularity but by how big the follower gets. In 01 a single dot introduces the lagged chase, 02 and 03 widen it to whole stages that react, 04 and 05 queue letters and particles behind the cursor, 06 through 08 put real UI parts in motion, and 09 ends with a picture that stretches with pointer speed. Every one runs the same lerp, so each demo is a variation of one piece of code.

01Follow basics — the lagged chase

The foundation of every chase: one lerp line moves the element a fraction of the remaining gap each frame, and three ghosts expose that lag as a frame difference.

Follow basics — the lagged chase
Open in a new tab

01 · Follow basics — the lagged chase — Measured area 3.684% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

pointermoverequestAnimationFrametranslate3d

One lerp line makes the delay

function lerp(a, b, k) { return a + (b - a) * k; }

function tick() {
  dot.x = lerp(dot.x, target.x, 0.16);
  dot.y = lerp(dot.y, target.y, 0.16);
  draw();
  requestAnimationFrame(tick);
}

With k at 0.16 the element covers only 16% of the gap per frame, so a stopped cursor leaves a gap that shrinks through 84%, 71%, 59% until the dot settles, and the three ghosts chase it in turn at 0.10, 0.065, and 0.04 to make the lag visible.

Measuring how far behind it sits

The render clock reported 3.68% changed area, intensity 120.2, and 23/23 moving frames, and a probe that actually moved the mouse saw the dot settle within 2.2px of the cursor 40 frames after the last move.

02A background that follows — three parallax layers

Two hills and the grass chase the cursor x at different ratios, blades lean toward the pointer with skewX, and the sun's pupils track it too.

A background that follows — three parallax layers
Open in a new tab

02 · A background that follows — three parallax layers — Measured area 6.548% · moving frames 15/23Original by GODRICH · SCSS and React ship in the zip · See the password

translate3dskewXvar(--px)

Each layer gets its own travel

pxNow = lerp(pxNow, px, 0.08);
scene.style.setProperty('--px', pxNow.toFixed(4));
var dx = pxNow - 0.5;
far.style.transform   = 'translate3d(' + (dx * -10).toFixed(1) + 'px,0,0)';
mid.style.transform   = 'translate3d(' + (dx * -22).toFixed(1) + 'px,0,0)';
grass.style.transform = 'translate3d(' + (dx * -34).toFixed(1) + 'px,0,0)';

--px holds the cursor x as a number from 0 to 1; all nine blades share that one value through skewX(calc((0.5 - var(--px)) * 26deg)), and the far hill drifts -10px, the near hill -22px, and the grass -34px, so the layers split into parallax.

The frame where parallax shows up as a number

The stage shifted 6.55% of its area at intensity 123.7 over 15/23 moving frames, and holding the cursor at the right edge carried --px to 0.913 after 30 frames against a target of 0.963 while the front grass sat 13.9px to the left in the same frame.

03Light that follows — a spotlight

A radial-gradient blob sweeps the dark stage one beat behind the cursor, lighting the word and the stars it passes.

Light that follows — a spotlight
Open in a new tab

03 · Light that follows — a spotlight — Measured area 29.695% · moving frames 17/23Original by GODRICH · SCSS and React ship in the zip · See the password

radial-gradientmix-blend-moderequestAnimationFrame

The material of the light

.sl__light {
  width: 220px; height: 220px;
  background: radial-gradient(circle, rgba($subject-cream, .95) 0%,
              rgba($stage-yellow, .38) 38%, rgba($stage-yellow, 0) 68%);
  mix-blend-mode: screen;
  border-radius: 50%;
}

Because screen blends brighten only what is already dark, the word surfaces under the light and sinks back once the light passes, and the chase factor is a slow-drifting 0.09.

The arithmetic of slow light

This stage changed the largest area of the set at 29.70% with intensity 45.0 and 17/23 moving frames, and pinning the cursor for 30 frames still left the light about 8% of its original distance away — the slow chase you can feel as a number.

04Letters that follow — a text train

Split a word into letters, let the first chase the cursor and each later letter chase the one ahead, and the word queues behind the cursor like a train.

Letters that follow — a text train
Open in a new tab

04 · Letters that follow — a text train — Measured area 10.345% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

pointermovewill-changetranslate3d

The chain that chases the letter ahead

var follow = [0.22, 0.16, 0.12, 0.09, 0.07];

function tick() {
  spots[0].x = lerp(spots[0].x, target.x, follow[0]);
  spots[0].y = lerp(spots[0].y, target.y - 34, follow[0]);
  for (var i = 1; i < spots.length; i++) {
    spots[i].x = lerp(spots[i].x, spots[i - 1].x, follow[i]);
    spots[i].y = lerp(spots[i].y, spots[i - 1].y + 8, follow[i]);
  }
  draw();
  requestAnimationFrame(tick);
}

Shrinking factors toward the back are the point, because a sharp cursor turn flings the tail letters outward, and each letter carries will-change: transform so the browser builds its compositing layer up front.

The overlap the preview caught

The frames showed 10.35% area, intensity 120.3, and 23/23 moving frames, but the first preview ran all five letters along one path with delays alone and two of them piled into a single blob at the right edge of a sampled frame, so each letter now starts from a slightly offset origin and the train reads as a diagonal.

05A fancy particle mouse — DOM confetti

No canvas: span shards burst in proportion to cursor speed and fly off with an element.animate() call, then vanish.

A fancy particle mouse — DOM confetti
Open in a new tab

05 · A fancy particle mouse — DOM confetti — Measured area 2.71% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

element.animate()pointermoveopacity

Distance triggers the burst

function spawn(x, y, dx, dy) {
  var element = pool[next];
  next = (next + 1) % pool.length;
  element.className = 'pb__p ' + shapes[(Math.random() * shapes.length) | 0];
  element.style.setProperty('--pc', colors[(Math.random() * colors.length) | 0]);
  var ang = Math.atan2(dy, dx) + (Math.random() - 0.5) * 2.4;
  var dist = 34 + Math.random() * 46;
  var tx = x + Math.cos(ang) * dist;
  var ty = y + Math.sin(ang) * dist;
  element.animate([
    { transform: 'translate3d(' + (x - 5) + 'px,' + (y - 5) + 'px,0) rotate(0deg) scale(1)', opacity: 1 },
    { transform: 'translate3d(' + (tx - 5) + 'px,' + (ty - 5) + 'px,0) rotate(200deg) scale(.15)', opacity: 0 }
  ], { duration: 620 + Math.random() * 320, easing: 'cubic-bezier(.2,.8,.2,1)' });
}

One shard fires every 26px of travel so faster sweeps pour more confetti, the three shapes take their color from a single --pc property, and the canvas side of this axis continues in the canvas particle engine.

Counting live animations

Moving the pointer hard raised the document's live animation count to 41, and 1.2 seconds after stopping it fell back to 0 because finished shards leave no fill and return to opacity: 0, with the render measuring 2.71% area, intensity 112.1, and 23/23 moving frames.

06Nav cursor effect — a sliding pill

A pill highlight glides to whichever menu item the cursor is on, and the logo's pupils track the cursor x.

Nav cursor effect — a sliding pill
Open in a new tab

06 · Nav cursor effect — a sliding pill — Measured area 3.0% · moving frames 16/23Original by GODRICH · SCSS and React ship in the zip · See the password

pointerovergetBoundingClientRecttranslate3d

Measure the slot, then lerp toward it

function measure(el) {
  var r = el.getBoundingClientRect();
  var w = wrap.getBoundingClientRect();
  return { x: r.left - w.left, w: r.width };
}

function tick() {
  now.x = lerp(now.x, slot.x, 0.16);
  now.w = lerp(now.w, slot.w, 0.16);
  pill.style.transform = 'translate3d(' + now.x.toFixed(1) + 'px,0,0)';
  pill.style.width = now.w.toFixed(1) + 'px';
  requestAnimationFrame(tick);
}

On pointerenter the pill glides to the item's left edge measured with getBoundingClientRect and stretches to its width, and since items are button elements the same measurement fires on focus, so keyboard tabs move the pill too.

Settling to the exact pixel

The render gave 3.00% area, intensity 129.2, and 16/23 moving frames, and after dispatching pointerenter on the second item the pill's x settled at 44.9px, exactly matching the item's real x of 44.9px within 0.7 seconds, while the preview loop's keyframes take the measured slots from CSS variables on load so the pill never runs past the labels even when the font or the language changes the item widths.

07Card hover effect — glow ring and magnet

A radial-gradient glow ring chases the cursor inside the card, and the card itself leans toward the pointer with translation only — no 3D tilt.

Card hover effect — glow ring and magnet
Open in a new tab

07 · Card hover effect — glow ring and magnet — Measured area 17.272% · moving frames 19/23Original by GODRICH · SCSS and React ship in the zip · See the password

pointermoveradial-gradientvar(--gx)

The ring tracks card-space coordinates

function setTarget(cx, cy) {
  var r = card.getBoundingClientRect();
  t.x = Math.min(r.width, Math.max(0, cx - r.left));
  t.y = Math.min(r.height, Math.max(0, cy - r.top));
  pull.x = Math.max(-7, Math.min(7, (t.x - r.width / 2) * 0.09));
  pull.y = Math.max(-7, Math.min(7, (t.y - r.height / 2) * 0.12));
}

The ring's position lives in --gx and --gy and moves through translate3d(calc(var(--gx) * 1px - 42px), calc(var(--gy) * 1px - 42px), 0) in the SCSS; the properties must be set on the ring element itself so the math stays bound to it, and the magnet is a clamp that pulls the card up to 7px toward the cursor.

The ring never leaves the card

This is the easiest place to start if you search with phrases like a mouse glow effect css or a hover glow, because the seed is eight lines of JS and the CSS only holds the position, and the render measured 17.27% area, intensity 71.5, and 19/23 moving frames, with the ring reaching --gx 263.9 and --gy 108.6 while a cursor outside the card clamps to the edge.

08Hover image preview — with a ribbon tail

Hover a row and its picture floats up, chasing the cursor with a lag, a ribbon name tag riding along.

Hover image preview — with a ribbon tail
Open in a new tab

08 · Hover image preview — with a ribbon tail — Measured area 12.68% · moving frames 18/23Original by GODRICH · SCSS and React ship in the zip · See the password

pointerovertranslate3drotate

Swap the scene, tilt while chasing

function tick() {
  p.x = lerp(p.x, t.x, 0.13);
  p.y = lerp(p.y, t.y, 0.13);
  rotNow = lerp(rotNow, rot, 0.1);
  rot = lerp(rot, 0, 0.06);
  preview.style.transform = 'translate3d(' + (p.x - 75).toFixed(1) + 'px,'
    + (p.y - 55).toFixed(1) + 'px,0) rotate(' + rotNow.toFixed(1) + 'deg)';
  requestAnimationFrame(tick);
}

The three scenes sit stacked at one size and swap through opacity, and a tilt of up to 7 degrees built from horizontal pointer travel rises and falls, so the picture feels dragged along by the cursor.

The row changes, the ribbon follows

The render measured 12.68% area, intensity 91.5, and 18/23 moving frames; dispatching pointerenter on the second row flipped the visible scene index to 1 and rewrote the ribbon to that row's name, and the scenes are three gradients with no photo files, so the zip opens anywhere.

09Image warp tracking — velocity stretch

The picture grabs the cursor's per-frame travel, stretches that way with skewX and scale, and springs back when the pointer stops.

Image warp tracking — velocity stretch
Open in a new tab

09 · Image warp tracking — velocity stretch — Measured area 5.497% · moving frames 16/23Original by GODRICH · SCSS and React ship in the zip · See the password

skewXscalerequestAnimationFrame

Turning speed into distortion

function tick() {
  sx = lerp(sx, vx, 0.24);
  sy = lerp(sy, vy, 0.24);
  vx = lerp(vx, 0, 0.12);
  vy = lerp(vy, 0, 0.12);
  var skew = Math.max(-11, Math.min(11, sx * 0.32));
  var stretch = 1 + Math.min(0.14, Math.abs(sx) * 0.011 + Math.abs(sy) * 0.009);
  art.style.transform = 'skewX(' + skew.toFixed(2) + 'deg) scale(' + stretch.toFixed(3) + ')';
  requestAnimationFrame(tick);
}

The velocities vx and vy kill 12% of themselves every frame, so a stopped cursor leaves the values easing through zero and the picture snaps back with a short afterimage of the stretch.

How far it stretched

The render measured 5.50% area, intensity 63.6, and 16/23 moving frames, and sweeping the cursor back and forth drove the picture to skewX(-11deg) scale(1.14) before it returned to skewX(0deg) scale(1) after 1.5 seconds.

How far does a cursor follow animation css go with no library?

Four ingredients carry all nine demos. pointermove supplies coordinates, requestAnimationFrame runs one lerp line per frame, transform is the only property that moves anything, and prefers-reduced-motion cuts the lag. Plenty of posts bolt a cursor follow animation css onto a library, but the nine above cover letters, light, backgrounds, shards, menus, cards, and pictures with just those four, and the physics interaction set picks up the same loop for springs and gravity.

One factor sets the personality

Factor Where Personality
0.22 04 first letter rides right on the cursor
0.16 01 dot, 06 pill the default one-beat delay
0.09 03 light a slow stage lamp
0.04 01 last ghost a tail that arrives late

Decide which axis of mouse effect you need

This post covers the thing that follows axis. Cursor styling itself lives in the mouse effects roundup, and 3D scenes that orbit the pointer live in the pointer 3D scenes post, so cursor-skin hunters get answers there while backgrounds, letters, and cards answering the cursor start here.

Where it breaks — a preview that moves but a page that doesn't

A dead feature check passed silently

The first version of 05 gated every burst behind typeof Element.animate === 'function', an expression that is false in every browser because animate lives on the prototype and never on the Element constructor, so the guard always failed and not a single shard fired while the preview loop kept playing. The measurement gate watches only the .is-demo CSS keyframes, so the dead path sailed through, and only a probe that moved the mouse and counted zero live animations caught it — after the fix the count read 41 while moving. Code whose preview runs but whose hands-on path is dead looks exactly like this.

Translate percentages measure the element, not the stage

The first preview path for 01 used values like translate3d(78%, 22px, 0) and the cursor ring barely moved, because a translate percentage resolves against the element's own width and 78% of an 18px ring is 14px. Rewriting the whole path in px fixed it, and the same class of mistake piled five train letters into one blob until their origins were spread, which is why a frame is not "seen" until a human has looked at it.

The zip with all nine demos plus the React ports opens with the password abmheerh, and on touch screens the position-chasing demos also listen for pointerdown so a finger drives the same chase.

Three variants and accessibility

Name Changed value Feel
duration $dur-enter 600ms → $dur-quick 220ms catches up faster and hugs the cursor
easing $ease-out → $ease-pop bounces once on arrival
color $subject-blue → $stage-yellow the follower moves from cold to warm

Accessibility works in two layers. A prefers-reduced-motion block stops the preview loops and shows each demo's resting pose in CSS, and the JS side reads the same setting to skip the lerp chase and snap straight to the pointer, so the light in 03 stays centered and the picture in 09 never stretches. The pill in 06 also moves on the buttons' focus for keyboard travel, and on screens without a pointer the seven position-chasing demos listen for pointerdown while the hover-driven 06 and 08 rely on pointerenter and focus.

FAQ

Do mouse follow effects work on smartphones?

Yes. The seven demos that chase a position also listen for pointerdown, so a touch or drag position drives the chase. Touch has no visible cursor, though, so effects where a whole surface reacts — like 02 and 07 — read more easily than the letter train that queues behind one.

Why not just use a transition instead of lerp?

Re-arming a transition on every pointer event makes the destinations fight each other, so the element jitters when events arrive in a burst. A lerp loop decays in proportion to the remaining gap no matter how often the target moves, which keeps multi-follower setups like 04 and 05 stable.

Won't more particles slow the page down?

This design recycles eight shards from a pool, so the DOM node count never grows. The probe counted at most 41 simultaneously live animations, and each one cleans itself up within a second; for thousands of particles the canvas particle engine is the right tool.

Sources

Enter the archive password

The password is inside this article. You will find it as you read.