カーソルトレイル
canvasにマウスの軌跡を残光として描く虹色トレイル。ヒーローセクションやランディングページの遊び心ある背景演出に向いています。
ライブデモ
使用例(お題: アイドルグループ Sakura)
この技法を「アイドルグループ Sakura」というテーマのダミーサイトで実際に使った例です。
HTML
<!-- Sakura: アイドルのライブ告知1画面。canvasの虹色トレイルを主役に -->
<div class="sk" data-trail-root>
<canvas class="sk__canvas" data-canvas></canvas>
<header class="sk__bar">
<span class="sk__logo">🌸 Sakura</span>
<nav class="sk__nav">
<a href="#">LIVE</a>
<a href="#">MUSIC</a>
<a href="#">NEWS</a>
</nav>
</header>
<section class="sk__hero">
<p class="sk__tag">SPRING TOUR 2026</p>
<h1 class="sk__title">桜咲くアリーナへ。</h1>
<p class="sk__lead">5人がつむぐ春のステージ。<br>あなたの声で、夜空をピンクに染めよう。</p>
<div class="sk__cta">
<span class="sk__btn">チケット先行受付</span>
<span class="sk__date">2026.04.05 / 横浜</span>
</div>
<p class="sk__hint">画面の上でカーソルを動かすと光の軌跡が舞います</p>
</section>
</div>
CSS
/* Sakura アイドルテーマ: 桜ピンク/白/淡グレー */
* { box-sizing: border-box; }
body {
margin: 0;
font-family: "Hiragino Sans", "Yu Gothic", system-ui, sans-serif;
color: #4a3a40;
overflow: hidden;
}
.sk {
position: relative;
height: 400px;
background:
radial-gradient(circle at 78% 22%, #ffd1e0 0%, transparent 52%),
radial-gradient(circle at 16% 86%, #ffe3ec 0%, transparent 50%),
linear-gradient(160deg, #fff 0%, #fdf4f7 100%);
overflow: hidden;
}
/* トレイル描画用canvas(背景の上、UIの下) */
.sk__canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
z-index: 1;
pointer-events: none;
}
/* ヘッダー */
.sk__bar {
position: relative;
z-index: 2;
display: flex;
align-items: center;
gap: 22px;
padding: 16px 28px;
font-size: 14px;
}
.sk__logo {
font-weight: 800;
letter-spacing: .06em;
font-size: 17px;
color: #e06a92;
}
.sk__nav {
display: flex;
gap: 18px;
margin-left: auto;
font-weight: 700;
letter-spacing: .08em;
font-size: 12px;
}
.sk__nav a { color: #8a6f78; text-decoration: none; }
/* ヒーロー */
.sk__hero {
position: relative;
z-index: 2;
height: calc(400px - 56px);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
gap: 12px;
padding: 0 28px;
pointer-events: none; /* トレイルを画面全体で楽しめるように */
}
.sk__tag {
margin: 0;
font-size: 12px;
font-weight: 800;
letter-spacing: .26em;
color: #e06a92;
}
.sk__title {
margin: 0;
font-size: 36px;
font-weight: 800;
letter-spacing: .03em;
color: #3a2b30;
text-shadow: 0 2px 14px rgba(255,209,224,.8);
}
.sk__lead {
margin: 0;
font-size: 13px;
line-height: 1.8;
color: #7a626b;
}
.sk__cta {
display: inline-flex;
align-items: center;
gap: 14px;
margin-top: 6px;
}
.sk__btn {
padding: 11px 24px;
font-size: 13px;
font-weight: 700;
color: #fff;
background: linear-gradient(135deg, #ff9bbb, #e06a92);
border-radius: 999px;
box-shadow: 0 12px 26px rgba(224,106,146,.4);
}
.sk__date {
font-size: 12px;
font-weight: 600;
color: #8a6f78;
}
.sk__hint {
margin: 4px 0 0;
font-size: 11px;
letter-spacing: .04em;
color: #b39aa2;
}
JavaScript
// Sakura: canvasの虹色トレイル。待機中は仮想カーソルが自動で舞い、操作で本物に追従
(() => {
const root = document.querySelector('[data-trail-root]');
const canvas = document.querySelector('[data-canvas]');
if (!root || !canvas) return; // null安全
const ctx = canvas.getContext('2d');
if (!ctx) return;
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
// 高解像度対応
let dpr = Math.min(window.devicePixelRatio || 1, 2);
const resize = () => {
const r = root.getBoundingClientRect();
canvas.width = Math.max(1, r.width * dpr);
canvas.height = Math.max(1, r.height * dpr);
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
};
resize();
window.addEventListener('resize', resize);
const trail = [];
const MAX = reduce ? 14 : 28;
let mx = -100, my = -100;
let usePointer = false; // 本物追従中か
let lastMove = 0;
const IDLE = 1500; // 無操作で自動巡回へ(ms)
root.addEventListener('pointermove', (e) => {
const r = root.getBoundingClientRect();
mx = e.clientX - r.left;
my = e.clientY - r.top;
usePointer = true;
lastMove = performance.now();
});
root.addEventListener('pointerleave', () => { usePointer = false; });
// 仮想カーソルの自動経路: ステージを横切るように舞う
const autoPos = (t) => {
const r = root.getBoundingClientRect();
const cx = r.width / 2, cy = r.height * 0.52;
return {
x: cx + Math.sin(t * 0.0009) * r.width * 0.34,
y: cy + Math.sin(t * 0.0015 + 1.2) * r.height * 0.22,
};
};
// 桜ピンク寄りの虹色(ピンク〜紫〜橙)
const hueAt = (s) => (320 + s * 120) % 360;
const draw = (now) => {
if (usePointer && now - lastMove > IDLE) usePointer = false;
if (!usePointer) {
const p = autoPos(now);
mx = p.x; my = p.y;
}
const w = canvas.width / dpr;
const h = canvas.height / dpr;
ctx.clearRect(0, 0, w, h);
trail.unshift({ x: mx, y: my });
if (trail.length > MAX) trail.pop();
// 発光する尾
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
for (let i = 0; i < trail.length - 1; i++) {
const a = trail[i];
const b = trail[i + 1];
const s = i / MAX;
const alpha = (1 - s) * 0.85;
ctx.strokeStyle = `hsla(${hueAt(s)}, 90%, 72%, ${alpha})`;
ctx.lineWidth = (1 - s) * 16 + 2;
ctx.beginPath();
ctx.moveTo(a.x, a.y);
ctx.lineTo(b.x, b.y);
ctx.stroke();
}
// 先頭の明るいコア
if (trail.length) {
const head = trail[0];
ctx.fillStyle = 'rgba(255,255,255,0.95)';
ctx.beginPath();
ctx.arc(head.x, head.y, 5, 0, Math.PI * 2);
ctx.fill();
}
requestAnimationFrame(draw);
};
requestAnimationFrame(draw);
})();
コード
HTML
<!-- カーソルトレイル:canvasに残光の軌跡を描く -->
<div class="stage" data-trail-root>
<canvas class="trail-canvas" data-canvas></canvas>
<div class="content">
<h1 class="title">カーソルトレイル</h1>
<p class="lead">マウスを動かすと、光の粒が尾を引いて流れます。</p>
</div>
</div>
CSS
* { box-sizing: border-box; }
html, body { margin: 0; height: 100%; }
.stage {
position: relative;
height: 360px;
overflow: hidden;
display: grid;
place-items: center;
background:
radial-gradient(800px 600px at 50% 50%, #1a1140 0%, transparent 70%),
#07060f;
color: #f3f0ff;
font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
cursor: none;
}
/* canvasは背面いっぱいに敷く */
.trail-canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
display: block;
}
.content {
position: relative;
z-index: 1;
text-align: center;
padding: 24px;
pointer-events: none; /* テキストはトレイルを邪魔しない */
}
.title {
margin: 0 0 12px;
font-size: clamp(32px, 6.5vw, 52px);
font-weight: 800;
letter-spacing: .03em;
background: linear-gradient(90deg, #ff9ad4, #9ab8ff, #8affe6);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.lead { margin: 0; color: #c7c2e8; font-size: 14px; }
JavaScript
// カーソルトレイル:マウス位置にパーティクルを生成し、フェードしながら追従
(() => {
const root = document.querySelector('[data-trail-root]');
const canvas = document.querySelector('[data-canvas]');
if (!root || !canvas) return; // null安全
const ctx = canvas.getContext('2d');
if (!ctx) return;
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
// 高解像度対応:実ピクセルに合わせてスケール
let dpr = Math.min(window.devicePixelRatio || 1, 2);
const resize = () => {
const r = root.getBoundingClientRect();
canvas.width = Math.max(1, r.width * dpr);
canvas.height = Math.max(1, r.height * dpr);
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
};
resize();
window.addEventListener('resize', resize);
// トレイル用の点列。新しい点ほど先頭に追加
const trail = [];
const MAX = reduce ? 12 : 26;
let mx = -100, my = -100, hasMoved = false;
root.addEventListener('pointermove', (e) => {
const r = root.getBoundingClientRect();
mx = e.clientX - r.left;
my = e.clientY - r.top;
hasMoved = true;
});
root.addEventListener('pointerleave', () => { hasMoved = false; });
// 虹色のグラデを線分で描く
const hueAt = (t) => (200 + t * 160) % 360;
const draw = () => {
const w = canvas.width / dpr;
const h = canvas.height / dpr;
ctx.clearRect(0, 0, w, h);
if (hasMoved) {
trail.unshift({ x: mx, y: my });
if (trail.length > MAX) trail.pop();
} else if (trail.length) {
trail.pop(); // マウスが離れたら尾を縮める
}
// 線分を重ねて発光する尾を描画
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
for (let i = 0; i < trail.length - 1; i++) {
const a = trail[i];
const b = trail[i + 1];
const t = i / MAX;
const alpha = (1 - t) * 0.9;
ctx.strokeStyle = `hsla(${hueAt(t)}, 95%, 65%, ${alpha})`;
ctx.lineWidth = (1 - t) * 18 + 2;
ctx.beginPath();
ctx.moveTo(a.x, a.y);
ctx.lineTo(b.x, b.y);
ctx.stroke();
}
// 先頭に明るいコアの円
if (trail.length) {
const head = trail[0];
ctx.fillStyle = 'rgba(255,255,255,0.95)';
ctx.beginPath();
ctx.arc(head.x, head.y, 6, 0, Math.PI * 2);
ctx.fill();
}
requestAnimationFrame(draw);
};
requestAnimationFrame(draw);
})();
🤖 AIエージェント用プロンプト
このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「カーソルトレイル」の効果を追加してください。
# 追加してほしい効果
カーソルトレイル(カスタムカーソル)
canvasにマウスの軌跡を残光として描く虹色トレイル。ヒーローセクションやランディングページの遊び心ある背景演出に向いています。
# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】
# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- カーソルトレイル:canvasに残光の軌跡を描く -->
<div class="stage" data-trail-root>
<canvas class="trail-canvas" data-canvas></canvas>
<div class="content">
<h1 class="title">カーソルトレイル</h1>
<p class="lead">マウスを動かすと、光の粒が尾を引いて流れます。</p>
</div>
</div>
【CSS】
* { box-sizing: border-box; }
html, body { margin: 0; height: 100%; }
.stage {
position: relative;
height: 360px;
overflow: hidden;
display: grid;
place-items: center;
background:
radial-gradient(800px 600px at 50% 50%, #1a1140 0%, transparent 70%),
#07060f;
color: #f3f0ff;
font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
cursor: none;
}
/* canvasは背面いっぱいに敷く */
.trail-canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
display: block;
}
.content {
position: relative;
z-index: 1;
text-align: center;
padding: 24px;
pointer-events: none; /* テキストはトレイルを邪魔しない */
}
.title {
margin: 0 0 12px;
font-size: clamp(32px, 6.5vw, 52px);
font-weight: 800;
letter-spacing: .03em;
background: linear-gradient(90deg, #ff9ad4, #9ab8ff, #8affe6);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.lead { margin: 0; color: #c7c2e8; font-size: 14px; }
【JavaScript】
// カーソルトレイル:マウス位置にパーティクルを生成し、フェードしながら追従
(() => {
const root = document.querySelector('[data-trail-root]');
const canvas = document.querySelector('[data-canvas]');
if (!root || !canvas) return; // null安全
const ctx = canvas.getContext('2d');
if (!ctx) return;
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
// 高解像度対応:実ピクセルに合わせてスケール
let dpr = Math.min(window.devicePixelRatio || 1, 2);
const resize = () => {
const r = root.getBoundingClientRect();
canvas.width = Math.max(1, r.width * dpr);
canvas.height = Math.max(1, r.height * dpr);
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
};
resize();
window.addEventListener('resize', resize);
// トレイル用の点列。新しい点ほど先頭に追加
const trail = [];
const MAX = reduce ? 12 : 26;
let mx = -100, my = -100, hasMoved = false;
root.addEventListener('pointermove', (e) => {
const r = root.getBoundingClientRect();
mx = e.clientX - r.left;
my = e.clientY - r.top;
hasMoved = true;
});
root.addEventListener('pointerleave', () => { hasMoved = false; });
// 虹色のグラデを線分で描く
const hueAt = (t) => (200 + t * 160) % 360;
const draw = () => {
const w = canvas.width / dpr;
const h = canvas.height / dpr;
ctx.clearRect(0, 0, w, h);
if (hasMoved) {
trail.unshift({ x: mx, y: my });
if (trail.length > MAX) trail.pop();
} else if (trail.length) {
trail.pop(); // マウスが離れたら尾を縮める
}
// 線分を重ねて発光する尾を描画
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
for (let i = 0; i < trail.length - 1; i++) {
const a = trail[i];
const b = trail[i + 1];
const t = i / MAX;
const alpha = (1 - t) * 0.9;
ctx.strokeStyle = `hsla(${hueAt(t)}, 95%, 65%, ${alpha})`;
ctx.lineWidth = (1 - t) * 18 + 2;
ctx.beginPath();
ctx.moveTo(a.x, a.y);
ctx.lineTo(b.x, b.y);
ctx.stroke();
}
// 先頭に明るいコアの円
if (trail.length) {
const head = trail[0];
ctx.fillStyle = 'rgba(255,255,255,0.95)';
ctx.beginPath();
ctx.arc(head.x, head.y, 6, 0, Math.PI * 2);
ctx.fill();
}
requestAnimationFrame(draw);
};
requestAnimationFrame(draw);
})();
# 外部ライブラリ
なし(追加ライブラリ不要)
# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。