Dot Cursor Follower
A small dot replaces the native cursor and tracks the pointer inside its panel.
3 個のフレームワーク初級
A comet trail of fading particles that follows the pointer across the panel.
<!--
A fixed pool of dots (no allocation per move) is cycled as the pointer moves;
a single rAF loop decays each dot's opacity and scale. The trail is pure
decoration, so reduced motion skips it and the panel is a plain card.
-->
<div id="cursor-trail-stage" class="relative w-full max-w-lg overflow-hidden rounded-2xl border border-gray-200 bg-gray-50 dark:border-gray-800 dark:bg-gray-900">
<div class="flex h-56 items-center justify-center px-6 text-center">
<p class="text-sm text-gray-600 dark:text-gray-400">Drag your pointer across for a comet trail</p>
</div>
<div id="cursor-trail-layer" class="pointer-events-none absolute inset-0" aria-hidden="true"></div>
</div>
<script>
(() => {
const stage = document.getElementById('cursor-trail-stage');
const layer = document.getElementById('cursor-trail-layer');
if (!stage || !layer || !matchMedia('(pointer: fine)').matches) return;
if (matchMedia('(prefers-reduced-motion: reduce)').matches) return;
const count = 16, color = 'rgb(59 130 246)';
let rect = stage.getBoundingClientRect();
const dots = [], xs = [], ys = [], lives = [];
for (let i = 0; i < count; i++) {
const d = document.createElement('span');
d.style.cssText = 'position:absolute;left:0;top:0;width:10px;height:10px;margin:-5px 0 0 -5px;border-radius:9999px;opacity:0;background:' + color;
layer.appendChild(d); dots.push(d); xs.push(0); ys.push(0); lives.push(0);
}
let head = 0, raf = 0;
const loop = () => {
let alive = false;
for (let i = 0; i < count; i++) {
if (lives[i] > 0) {
lives[i] = Math.max(0, lives[i] - 0.045);
dots[i].style.opacity = String(lives[i]);
dots[i].style.transform = 'translate3d(' + xs[i] + 'px,' + ys[i] + 'px,0) scale(' + lives[i] + ')';
alive = true;
}
}
raf = alive ? requestAnimationFrame(loop) : 0;
};
stage.addEventListener('pointerenter', () => { rect = stage.getBoundingClientRect(); });
stage.addEventListener('pointermove', (e) => {
xs[head] = e.clientX - rect.left; ys[head] = e.clientY - rect.top; lives[head] = 1;
head = (head + 1) % count;
if (!raf) raf = requestAnimationFrame(loop);
});
})();
</script>| Prop | 型 | デフォルト | 説明 |
|---|---|---|---|
hint | string | 'Drag your pointer across for a comet trail' | Hint |
color | string | 'rgb(59 130 246)' | Color |
count | number | 16 | バッジに表示される数値。99 を超えると「99+」と表示されます。 |
className | string | - | ルート要素にマージされる追加クラス。 |
Set particle `color` and pool `count` (also the trail length). A fixed pool avoids per-move allocation and one rAF loop decays opacity and scale; the trail is pure decoration, so reduced motion opts out.