Move your mouse anywhere on this page. A trail of glowing fireflies follows, drifts slightly upward, then fades. It's one of those tiny choices that turns a site from finished into alive.
Pick a color and the fireflies on the entire page change instantly.
Reach for it on personal sites, portfolios, landing pages, or any site where the visitor's attention is the product. It's a 2-second wow that costs nothing in performance. Pairs beautifully with dark backgrounds and high-contrast typography.
Skip it on data-heavy interfaces (dashboards, spreadsheets) or anywhere the
cursor needs to read as precise (e.g., design tools). The component auto-disables
for users with prefers-reduced-motion, so accessibility is handled.
No HTML changes. Just drop the CSS + JS in any page. The whole document becomes the firefly canvas.
<!-- That's it. No markup needed. Just paste the CSS + JS below into any page. -->
One element style + one keyframe. The CSS variable --firefly-color lets you swap palette globally without rewriting.
:root { --firefly-color: #ffd23f; /* swap to any color */ } .firefly-trail { position: absolute; border-radius: 50%; background: var(--firefly-color); box-shadow: 0 0 10px var(--firefly-color), 0 0 22px rgba(255, 210, 63, .4); pointer-events: none; transform: translate(-50%, -50%); animation: firefly-fade 2.2s ease-out forwards; z-index: 5000; } @keyframes firefly-fade { 0% { opacity: .95; transform: translate(-50%, -50%) scale(1); } 100% { opacity: 0; transform: translate(calc(-50% + var(--dx)), calc(-50% + var(--dy))) scale(.4); } }
Throttles the mousemove event so it spawns ~30 fireflies/sec max. Each firefly randomizes size + drift direction, then removes itself when it fades. Respects prefers-reduced-motion.
// Swamp UI — Firefly Cursor (function () { // Respect accessibility preference if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; const RATE = 35; // ms between spawns (lower = denser trail) const SIZE_MIN = 3; // px const SIZE_MAX = 7; const LIFE_MS = 2200; // must match CSS animation duration const DRIFT_UP = 40; // px upward drift const DRIFT_X = 24; // px horizontal wobble let last = 0; document.addEventListener('mousemove', e => { const now = Date.now(); if (now - last < RATE) return; last = now; const f = document.createElement('span'); const size = SIZE_MIN + Math.random() * (SIZE_MAX - SIZE_MIN); const dx = (Math.random() - .5) * DRIFT_X; const dy = -DRIFT_UP * (.5 + Math.random() * .6); f.className = 'firefly-trail'; f.style.cssText = 'left:' + (e.clientX + window.scrollX) + 'px;' + 'top:' + (e.clientY + window.scrollY) + 'px;' + 'width:' + size + 'px;' + 'height:' + size + 'px;' + '--dx:' + dx + 'px;' + '--dy:' + dy + 'px;'; document.body.appendChild(f); setTimeout(() => f.remove(), LIFE_MS); }); // Optional: also spawn on touchmove for mobile document.addEventListener('touchmove', e => { if (e.touches.length === 0) return; const t = e.touches[0]; document.dispatchEvent(new MouseEvent('mousemove', { clientX: t.clientX, clientY: t.clientY })); }); })();
| Constant | Default | Effect |
|---|---|---|
RATE |
35 ms | Time between spawns. Lower = denser swarm; higher = sparser trail. |
SIZE_MIN / SIZE_MAX |
3 / 7 px | Firefly diameter range. Bigger = more dragon-fly-ish. |
LIFE_MS |
2200 ms | How long each firefly lives. Must match CSS animation duration. |
DRIFT_UP |
40 px | Upward drift distance during fade. Higher = floatier. |
DRIFT_X |
24 px | Horizontal wobble. Higher = more chaotic, like real fireflies. |
--firefly-color (CSS var) |
#ffd23f | Color of every firefly. Change in one place to swap palette globally. |
Pro tip: set --firefly-color on a section instead of :root
to give different page sections their own firefly color.