HERO SECTION浮遊パーティクル
<!-- 背景を敷きたい親(セクションやヒーロー)に position:relative を付けて、 その先頭にこの1行を置く。上に載せる中身は position:relative; z-index:1 に --> <div class="bnto-bgdots" data-bnto-bgdots data-count="40" aria-hidden="true"></div>
/* 親要素いっぱいに広がる背景レイヤー(親に position:relative が必要) */ .bnto-bgdots { --c1: hsl(205 85% 72%); /* 光点の色 */ position: absolute; inset: 0; overflow: hidden; pointer-events: none; } .bnto-bgdots canvas { display: block; width: 100%; height: 100%; }
// data-bnto-bgdots を自動検出して初期化(複数設置OK・二重実行OK) (function () { var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches; document.querySelectorAll('[data-bnto-bgdots]').forEach(function (bg) { if (bg.dataset.bntoInit) return; bg.dataset.bntoInit = '1'; var canvas = document.createElement('canvas'); bg.appendChild(canvas); var ctx = canvas.getContext('2d'); var count = parseInt(bg.dataset.count, 10) || 40; var dots = [], W = 0, H = 0, raf = 0, shown = true; function resize() { var dpr = window.devicePixelRatio || 1; // Retinaでもにじまない W = bg.clientWidth; H = bg.clientHeight; canvas.width = W * dpr; canvas.height = H * dpr; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); } function seed() { dots = []; for (var i = 0; i < count; i++) dots.push({ x: Math.random() * W, y: Math.random() * H, r: 1 + Math.random() * 2.5, vx: (Math.random() - .5) * .35, vy: (Math.random() - .5) * .35, a: .25 + Math.random() * .6 }); } function draw() { ctx.clearRect(0, 0, W, H); ctx.fillStyle = getComputedStyle(bg).getPropertyValue('--c1').trim() || '#9CC4EE'; dots.forEach(function (d) { ctx.globalAlpha = d.a; ctx.beginPath(); ctx.arc(d.x, d.y, d.r, 0, Math.PI * 2); ctx.fill(); }); ctx.globalAlpha = 1; } function tick() { dots.forEach(function (d) { d.x += d.vx; d.y += d.vy; if (d.x < -5) d.x = W + 5; if (d.x > W + 5) d.x = -5; if (d.y < -5) d.y = H + 5; if (d.y > H + 5) d.y = -5; }); draw(); raf = requestAnimationFrame(tick); } function start() { if (!raf && shown && !reduced) raf = requestAnimationFrame(tick); } function stop() { cancelAnimationFrame(raf); raf = 0; } resize(); seed(); draw(); // reduced-motion 時はこの静止画のまま if ('ResizeObserver' in window) new ResizeObserver(function () { resize(); draw(); }).observe(bg); else window.addEventListener('resize', function () { resize(); draw(); }); // 画面外にいるあいだは停止(パフォーマンス配慮) if ('IntersectionObserver' in window) new IntersectionObserver(function (en) { shown = en[0].isIntersecting; if (shown) start(); else stop(); }).observe(bg); else start(); }); })();
使い方のコツ
粒の数は data-count、色は --c1 で調整できます。モバイルでは30個前後に抑えるのがおすすめです。画面外に出ると IntersectionObserver が描画を自動停止するので、長いページに置いても無駄なCPU消費が発生しません。