30 CSS Hover Effects 29 / 30
CSS Dot Trail Cursor Hover Effect
A trail of fading dot particles follows the cursor inside a container — four trail styles including color rainbow, size decay, and comet tail.
This is a full-page demo — interact inside the frame above, or open it in the playground for the full-screen experience.
The code
<div class="hv-29">
<p class="hv-29__label">Dot Trail Cursor — 4 Styles</p>
<div class="hv-29__grid">
<div class="hv-29__zone hv-29__zone--classic" data-trail="classic">
<span class="hv-29__hint">Classic Fade</span>
</div>
<div class="hv-29__zone hv-29__zone--rainbow" data-trail="rainbow">
<span class="hv-29__hint">Rainbow</span>
</div>
<div class="hv-29__zone hv-29__zone--comet" data-trail="comet">
<span class="hv-29__hint">Comet Tail</span>
</div>
<div class="hv-29__zone hv-29__zone--sparkle" data-trail="sparkle">
<span class="hv-29__hint">Sparkle</span>
</div>
</div>
</div> <div class="hv-29">
<p class="hv-29__label">Dot Trail Cursor — 4 Styles</p>
<div class="hv-29__grid">
<div class="hv-29__zone hv-29__zone--classic" data-trail="classic">
<span class="hv-29__hint">Classic Fade</span>
</div>
<div class="hv-29__zone hv-29__zone--rainbow" data-trail="rainbow">
<span class="hv-29__hint">Rainbow</span>
</div>
<div class="hv-29__zone hv-29__zone--comet" data-trail="comet">
<span class="hv-29__hint">Comet Tail</span>
</div>
<div class="hv-29__zone hv-29__zone--sparkle" data-trail="sparkle">
<span class="hv-29__hint">Sparkle</span>
</div>
</div>
</div>.hv-29,
.hv-29 *,
.hv-29 *::before,
.hv-29 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.hv-29 {
font-family: system-ui, sans-serif;
background: #080810;
padding: 2rem;
min-height: 100vh;
user-select: none;
}
.hv-29__label {
text-align: center;
color: #444;
font-size: .72rem;
letter-spacing: .15em;
text-transform: uppercase;
margin-bottom: 1.5rem;
}
.hv-29__grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
max-width: 700px;
margin: 0 auto;
}
.hv-29__zone {
position: relative;
overflow: hidden;
border-radius: 12px;
aspect-ratio: 4/3;
border: 1px solid #1a1a2a;
cursor: none;
display: flex;
align-items: center;
justify-content: center;
}
.hv-29__zone--classic { background: #0d0d18; }
.hv-29__zone--rainbow { background: #0d0d18; }
.hv-29__zone--comet { background: #0a0a14; }
.hv-29__zone--sparkle { background: #0c0c18; }
.hv-29__hint {
color: #2a2a40;
font-size: .75rem;
letter-spacing: .1em;
text-transform: uppercase;
pointer-events: none;
}
/* ── Dot base ── */
.hv-29__dot {
position: absolute;
border-radius: 50%;
pointer-events: none;
transform: translate(-50%, -50%);
will-change: transform, opacity;
}
/* ── Classic: white dots fade & shrink ── */
@keyframes hv-29-classic {
0% { opacity: .9; transform: translate(-50%,-50%) scale(1); }
100% { opacity: 0; transform: translate(-50%,-50%) scale(0); }
}
.hv-29__zone--classic .hv-29__dot {
width: 8px; height: 8px;
background: #fff;
animation: hv-29-classic .6s ease-out forwards;
}
/* ── Rainbow: hue-rotated dots ── */
@keyframes hv-29-rainbow {
0% { opacity: 1; transform: translate(-50%,-50%) scale(1.1); }
100% { opacity: 0; transform: translate(-50%,-50%) scale(0.1); }
}
.hv-29__zone--rainbow .hv-29__dot {
width: 10px; height: 10px;
animation: hv-29-rainbow .7s ease-out forwards;
/* background set inline via JS */
}
/* ── Comet: elongated fade to right ── */
@keyframes hv-29-comet {
0% { opacity: 1; transform: translate(-50%,-50%) scale(1, 1); }
100% { opacity: 0; transform: translate(-50%,-50%) scale(3, .3); }
}
.hv-29__zone--comet .hv-29__dot {
width: 12px; height: 4px;
background: linear-gradient(90deg, rgba(100,180,255,0), rgba(100,180,255,1));
border-radius: 2px;
animation: hv-29-comet .5s ease-out forwards;
}
/* ── Sparkle: star-like burst ── */
@keyframes hv-29-sparkle {
0% { opacity: 1; transform: translate(-50%,-50%) scale(0) rotate(0deg); }
60% { opacity: 1; transform: translate(-50%,-50%) scale(1.3) rotate(60deg); }
100% { opacity: 0; transform: translate(-50%,-50%) scale(0.4) rotate(90deg); }
}
.hv-29__zone--sparkle .hv-29__dot {
width: 10px; height: 10px;
clip-path: polygon(50% 0%,61% 35%,98% 35%,68% 57%,79% 91%,50% 70%,21% 91%,32% 57%,2% 35%,39% 35%);
background: #f7d060;
animation: hv-29-sparkle .65s ease-out forwards;
}
@media (prefers-reduced-motion: reduce) {
.hv-29__dot { animation-duration: .01ms !important; }
} .hv-29,
.hv-29 *,
.hv-29 *::before,
.hv-29 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.hv-29 {
font-family: system-ui, sans-serif;
background: #080810;
padding: 2rem;
min-height: 100vh;
user-select: none;
}
.hv-29__label {
text-align: center;
color: #444;
font-size: .72rem;
letter-spacing: .15em;
text-transform: uppercase;
margin-bottom: 1.5rem;
}
.hv-29__grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
max-width: 700px;
margin: 0 auto;
}
.hv-29__zone {
position: relative;
overflow: hidden;
border-radius: 12px;
aspect-ratio: 4/3;
border: 1px solid #1a1a2a;
cursor: none;
display: flex;
align-items: center;
justify-content: center;
}
.hv-29__zone--classic { background: #0d0d18; }
.hv-29__zone--rainbow { background: #0d0d18; }
.hv-29__zone--comet { background: #0a0a14; }
.hv-29__zone--sparkle { background: #0c0c18; }
.hv-29__hint {
color: #2a2a40;
font-size: .75rem;
letter-spacing: .1em;
text-transform: uppercase;
pointer-events: none;
}
/* ── Dot base ── */
.hv-29__dot {
position: absolute;
border-radius: 50%;
pointer-events: none;
transform: translate(-50%, -50%);
will-change: transform, opacity;
}
/* ── Classic: white dots fade & shrink ── */
@keyframes hv-29-classic {
0% { opacity: .9; transform: translate(-50%,-50%) scale(1); }
100% { opacity: 0; transform: translate(-50%,-50%) scale(0); }
}
.hv-29__zone--classic .hv-29__dot {
width: 8px; height: 8px;
background: #fff;
animation: hv-29-classic .6s ease-out forwards;
}
/* ── Rainbow: hue-rotated dots ── */
@keyframes hv-29-rainbow {
0% { opacity: 1; transform: translate(-50%,-50%) scale(1.1); }
100% { opacity: 0; transform: translate(-50%,-50%) scale(0.1); }
}
.hv-29__zone--rainbow .hv-29__dot {
width: 10px; height: 10px;
animation: hv-29-rainbow .7s ease-out forwards;
/* background set inline via JS */
}
/* ── Comet: elongated fade to right ── */
@keyframes hv-29-comet {
0% { opacity: 1; transform: translate(-50%,-50%) scale(1, 1); }
100% { opacity: 0; transform: translate(-50%,-50%) scale(3, .3); }
}
.hv-29__zone--comet .hv-29__dot {
width: 12px; height: 4px;
background: linear-gradient(90deg, rgba(100,180,255,0), rgba(100,180,255,1));
border-radius: 2px;
animation: hv-29-comet .5s ease-out forwards;
}
/* ── Sparkle: star-like burst ── */
@keyframes hv-29-sparkle {
0% { opacity: 1; transform: translate(-50%,-50%) scale(0) rotate(0deg); }
60% { opacity: 1; transform: translate(-50%,-50%) scale(1.3) rotate(60deg); }
100% { opacity: 0; transform: translate(-50%,-50%) scale(0.4) rotate(90deg); }
}
.hv-29__zone--sparkle .hv-29__dot {
width: 10px; height: 10px;
clip-path: polygon(50% 0%,61% 35%,98% 35%,68% 57%,79% 91%,50% 70%,21% 91%,32% 57%,2% 35%,39% 35%);
background: #f7d060;
animation: hv-29-sparkle .65s ease-out forwards;
}
@media (prefers-reduced-motion: reduce) {
.hv-29__dot { animation-duration: .01ms !important; }
}(function () {
const zones = document.querySelectorAll('.hv-29__zone');
const RAINBOW = ['#ff6b6b','#ffa94d','#ffd43b','#69db7c','#4dabf7','#cc5de8','#f783ac'];
let hueIndex = 0;
let rafId = null;
let pendingZone = null;
let pendingX = 0;
let pendingY = 0;
function spawnDot(zone, x, y) {
const trail = zone.dataset.trail;
const dot = document.createElement('span');
dot.className = 'hv-29__dot';
dot.style.left = x + 'px';
dot.style.top = y + 'px';
if (trail === 'rainbow') {
dot.style.background = RAINBOW[hueIndex % RAINBOW.length];
hueIndex++;
}
zone.appendChild(dot);
dot.addEventListener('animationend', () => dot.remove(), { once: true });
}
function onMove(e) {
const zone = e.currentTarget;
const rect = zone.getBoundingClientRect();
pendingX = e.clientX - rect.left;
pendingY = e.clientY - rect.top;
pendingZone = zone;
if (!rafId) {
rafId = requestAnimationFrame(() => {
if (pendingZone) spawnDot(pendingZone, pendingX, pendingY);
rafId = null;
});
}
}
zones.forEach(zone => {
zone.addEventListener('mousemove', onMove);
zone.addEventListener('mouseleave', () => { pendingZone = null; });
});
})(); (function () {
const zones = document.querySelectorAll('.hv-29__zone');
const RAINBOW = ['#ff6b6b','#ffa94d','#ffd43b','#69db7c','#4dabf7','#cc5de8','#f783ac'];
let hueIndex = 0;
let rafId = null;
let pendingZone = null;
let pendingX = 0;
let pendingY = 0;
function spawnDot(zone, x, y) {
const trail = zone.dataset.trail;
const dot = document.createElement('span');
dot.className = 'hv-29__dot';
dot.style.left = x + 'px';
dot.style.top = y + 'px';
if (trail === 'rainbow') {
dot.style.background = RAINBOW[hueIndex % RAINBOW.length];
hueIndex++;
}
zone.appendChild(dot);
dot.addEventListener('animationend', () => dot.remove(), { once: true });
}
function onMove(e) {
const zone = e.currentTarget;
const rect = zone.getBoundingClientRect();
pendingX = e.clientX - rect.left;
pendingY = e.clientY - rect.top;
pendingZone = zone;
if (!rafId) {
rafId = requestAnimationFrame(() => {
if (pendingZone) spawnDot(pendingZone, pendingX, pendingY);
rafId = null;
});
}
}
zones.forEach(zone => {
zone.addEventListener('mousemove', onMove);
zone.addEventListener('mouseleave', () => { pendingZone = null; });
});
})();How this works
JavaScript tracks mousemove events and spawns absolutely-positioned elements at cursor coordinates. Each dot gets a CSS class (or inline custom property) and uses a CSS animation to fade out and shrink. Spawned dots are removed from the DOM after their animation ends via animationend. Four CSS @keyframes variants handle different visual effects.
Customize
- Change --dot-color for a solid color trail, --dot-size for dot radius, throttle spawn rate via the requestAnimationFrame interval, or swap in SVG icons instead of dots.
Watch out for
- Always use position:absolute dots inside a position:relative container — not the document body — to avoid scroll offset bugs.
- Remove dots on animationend, not with setTimeout, so duration changes in CSS automatically clean up correctly.
- Throttle spawning to every ~16ms (rAF) max; unthrottled mousemove can create thousands of elements per second.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| ✅ | ✅ | ✅ | ✅ |
animationend event universally supported. CSS custom properties on elements require modern browsers.
More from 30 CSS Hover Effects
CSS Curtain Reveal Image Hover EffectCSS Ken Burns Image Hover EffectCSS Image Tilt Depth Hover EffectCSS Underline Slide Nav Hover EffectCSS Highlight Fill Nav Hover EffectCSS Strikethrough Hover Link EffectCSS Inline Word Swap Hover EffectCSS Magnetic Cursor Pull Hover EffectCSS Underline Draw Hover EffectCSS Text Glitch Hover EffectCSS Letter Spacing Expand Hover EffectCSS Gradient Text Reveal Hover Effect
View the full collection →