弾力ジェリーカーソル
移動速度から伸び量と角度を計算し、進行方向へ伸縮する粘性カーソル。体積保存風のscaleで“びよん”とした有機的な動きを表現します。
ライブデモ
使用例(お題: SaaS FlowDesk)
この技法を「SaaS FlowDesk」というテーマのダミーサイトで実際に使った例です。
HTML
<!-- FlowDesk: SaaS機能紹介1画面。弾力ジェリーカーソルを主役に -->
<div class="fd" data-jelly-root>
<header class="fd__bar">
<span class="fd__logo"><span class="fd__mark"></span>FlowDesk</span>
<nav class="fd__nav">
<a href="#">機能</a>
<a href="#">料金</a>
<a href="#">ブログ</a>
</nav>
</header>
<section class="fd__body">
<div class="fd__intro">
<p class="fd__kicker">FEATURES</p>
<h1 class="fd__title">動きで、<br>使い心地が変わる。</h1>
<p class="fd__lead">タスク・通知・対話をひとつに。<br>触れるほど馴染む、なめらかな操作感。</p>
</div>
<ul class="fd__cards">
<li class="fd__card"><span class="fd__ico">⚡</span>高速タスク</li>
<li class="fd__card"><span class="fd__ico">🔔</span>賢い通知</li>
<li class="fd__card"><span class="fd__ico">📊</span>分析ボード</li>
<li class="fd__card"><span class="fd__ico">🔗</span>連携豊富</li>
</ul>
</section>
<p class="fd__hint">カーソルが進む方向へ“びよん”と伸びます</p>
<!-- 主役: 弾力ジェリーカーソル -->
<div class="jelly-cursor" data-jelly></div>
</div>
CSS
/* FlowDesk SaaSテーマ: 紺/青/白 */
* { box-sizing: border-box; }
body {
margin: 0;
font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
background:
radial-gradient(circle at 82% 16%, rgba(79,124,255,.28) 0%, transparent 44%),
radial-gradient(circle at 8% 90%, rgba(79,124,255,.14) 0%, transparent 48%),
#0f1b34;
color: #fff;
overflow: hidden;
cursor: none;
}
.fd {
position: relative;
height: 400px;
display: flex;
flex-direction: column;
padding: 0 30px;
}
/* ヘッダー */
.fd__bar {
display: flex;
align-items: center;
gap: 24px;
padding: 15px 0;
font-size: 14px;
}
.fd__logo {
display: inline-flex;
align-items: center;
gap: 9px;
font-weight: 800;
font-size: 16px;
}
.fd__mark {
width: 18px;
height: 18px;
border-radius: 6px;
background: linear-gradient(135deg, #4f7cff, #8fb0ff);
box-shadow: 0 0 14px rgba(79,124,255,.6);
}
.fd__nav {
display: flex;
gap: 20px;
margin-left: auto;
}
.fd__nav a { color: rgba(255,255,255,.72); text-decoration: none; }
/* 本文 */
.fd__body {
flex: 1;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 26px;
align-items: center;
}
.fd__kicker {
margin: 0 0 8px;
font-size: 12px;
font-weight: 700;
letter-spacing: .22em;
color: #8fb0ff;
}
.fd__title {
margin: 0 0 12px;
font-size: 28px;
line-height: 1.3;
font-weight: 800;
}
.fd__lead {
margin: 0;
font-size: 13px;
line-height: 1.8;
color: rgba(255,255,255,.66);
}
/* 機能カード */
.fd__cards {
list-style: none;
margin: 0;
padding: 0;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.fd__card {
display: flex;
align-items: center;
gap: 10px;
padding: 16px 14px;
font-size: 14px;
font-weight: 700;
background: rgba(255,255,255,.06);
border: 1px solid rgba(255,255,255,.12);
border-radius: 14px;
}
.fd__ico { font-size: 20px; }
.fd__hint {
margin: 0 0 14px;
text-align: center;
font-size: 12px;
letter-spacing: .04em;
color: rgba(255,255,255,.42);
}
/* 主役: 弾力ジェリーカーソル */
.jelly-cursor {
position: fixed;
top: 0;
left: 0;
width: 26px;
height: 26px;
margin: -13px 0 0 -13px;
border-radius: 50%;
background: radial-gradient(circle at 35% 30%, #aac4ff, #4f7cff 70%);
box-shadow: 0 0 18px rgba(79,124,255,.6);
pointer-events: none;
z-index: 50;
opacity: 0;
transition: opacity .3s ease;
}
.fd.is-active .jelly-cursor { opacity: 1; }
JavaScript
// FlowDesk: 弾力ジェリーカーソル。待機中は自動巡回で伸縮し、操作で本物に追従
(() => {
const root = document.querySelector('[data-jelly-root]');
const jelly = document.querySelector('[data-jelly]');
if (!root || !jelly) return; // null安全
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
// 現在位置・目標位置・前フレーム位置
let x = window.innerWidth / 2, y = window.innerHeight / 2;
let tx = x, ty = y;
let px = x, py = y;
let usePointer = false;
let lastMove = 0;
const IDLE = 1500;
root.classList.add('is-active');
root.addEventListener('pointermove', (e) => {
usePointer = true;
lastMove = performance.now();
tx = e.clientX; ty = e.clientY;
});
root.addEventListener('pointerleave', () => { usePointer = false; });
// 仮想カーソルの自動経路: 機能カード帯を横切るように動く
const autoPos = (t) => {
const r = root.getBoundingClientRect();
const cx = r.left + r.width / 2;
const cy = r.top + r.height * 0.5;
return {
x: cx + Math.sin(t * 0.0011) * r.width * 0.34,
y: cy + Math.sin(t * 0.0017 + 1.0) * r.height * 0.22,
};
};
const ease = reduce ? 1 : 0.2;
const tick = (now) => {
if (usePointer && now - lastMove > IDLE) usePointer = false;
if (!usePointer) {
const p = autoPos(now);
tx = p.x; ty = p.y;
}
// 目標へ補間
x += (tx - x) * ease;
y += (ty - y) * ease;
// 速度から伸びと回転を算出
const vx = x - px;
const vy = y - py;
px = x; py = y;
const speed = Math.min(Math.hypot(vx, vy), 80);
const angle = Math.atan2(vy, vx) * (180 / Math.PI);
const stretch = reduce ? 0 : speed / 120;
const sx = 1 + stretch;
const sy = 1 - stretch * 0.6;
// CSSで中心合わせ済みなので位置はtranslateのみ
jelly.style.transform =
`translate(${x}px, ${y}px) rotate(${angle}deg) scale(${sx}, ${sy})`;
requestAnimationFrame(tick);
};
requestAnimationFrame(tick);
})();
コード
HTML
<!-- 弾力ジェリーカーソル:速度に応じて伸び縮みする粘性カーソル -->
<div class="stage" data-jelly-root>
<div class="content">
<h1 class="title">ジェリーカーソル</h1>
<p class="lead">素早く動かすと、進行方向に“びよん”と伸びます。</p>
<div class="dots">
<span></span><span></span><span></span>
</div>
</div>
<!-- 弾力カーソル -->
<div class="jelly" data-jelly></div>
</div>
CSS
* { box-sizing: border-box; }
html, body { margin: 0; height: 100%; }
.stage {
position: relative;
height: 360px;
display: grid;
place-items: center;
overflow: hidden;
background:
radial-gradient(600px 360px at 30% 20%, #103b3a 0%, transparent 60%),
radial-gradient(600px 360px at 80% 90%, #2a1140 0%, transparent 60%),
#0a0d12;
color: #eafff8;
font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
cursor: none;
}
.content { text-align: center; padding: 24px; z-index: 1; }
.title {
margin: 0 0 10px;
font-size: clamp(30px, 6vw, 48px);
font-weight: 800;
letter-spacing: .03em;
background: linear-gradient(90deg, #6df0c2, #58c8ff);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.lead { margin: 0 0 22px; color: #9fc4bd; font-size: 14px; }
.dots { display: flex; gap: 14px; justify-content: center; }
.dots span {
width: 12px; height: 12px;
border-radius: 50%;
background: rgba(109,240,194,.5);
animation: pulse 1.8s ease-in-out infinite;
}
.dots span:nth-child(2) { animation-delay: .25s; }
.dots span:nth-child(3) { animation-delay: .5s; }
@keyframes pulse {
0%, 100% { transform: scale(1); opacity: .5; }
50% { transform: scale(1.5); opacity: 1; }
}
/* ジェリーカーソル本体:rotate+scaleで伸縮 */
.jelly {
position: fixed;
top: 0; left: 0;
width: 34px; height: 34px;
border-radius: 50%;
background: radial-gradient(circle at 35% 30%, #aeffe0, #2fd6a6 60%, #18b48a);
box-shadow: 0 0 20px rgba(47,214,166,.6);
pointer-events: none;
z-index: 9999;
will-change: transform;
opacity: 0; /* 初回移動まで非表示(中央の文字に重ならない) */
transition: opacity .3s ease;
}
[data-jelly-root].is-active .jelly { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
.dots span { animation: none; }
}
JavaScript
// 弾力ジェリーカーソル:速度ベクトルから伸び量と角度を求め、scale/rotateで変形
(() => {
const root = document.querySelector('[data-jelly-root]');
const jelly = document.querySelector('[data-jelly]');
if (!root || !jelly) return; // null安全
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
// 現在位置・目標位置・前フレーム位置
let x = window.innerWidth / 2, y = window.innerHeight / 2;
let tx = x, ty = y;
let px = x, py = y;
root.addEventListener('pointermove', (e) => {
tx = e.clientX;
ty = e.clientY;
if (!root.classList.contains('is-active')) root.classList.add('is-active');
});
root.addEventListener('pointerleave', () => root.classList.remove('is-active'));
const ease = reduce ? 1 : 0.2;
const tick = () => {
// 目標へ補間して滑らかに追従
x += (tx - x) * ease;
y += (ty - y) * ease;
// 速度(移動量)から伸びと回転を算出
const vx = x - px;
const vy = y - py;
px = x; py = y;
const speed = Math.min(Math.hypot(vx, vy), 80); // 上限でクランプ
const angle = Math.atan2(vy, vx) * (180 / Math.PI);
const stretch = reduce ? 0 : speed / 120; // 0〜0.66程度
// 進行方向に伸ばし、直交方向に縮める(体積保存風)
const sx = 1 + stretch;
const sy = 1 - stretch * 0.6;
jelly.style.transform =
`translate(${x}px, ${y}px) translate(-50%, -50%) ` +
`rotate(${angle}deg) scale(${sx}, ${sy})`;
requestAnimationFrame(tick);
};
requestAnimationFrame(tick);
})();
🤖 AIエージェント用プロンプト
このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「弾力ジェリーカーソル」の効果を追加してください。
# 追加してほしい効果
弾力ジェリーカーソル(カスタムカーソル)
移動速度から伸び量と角度を計算し、進行方向へ伸縮する粘性カーソル。体積保存風のscaleで“びよん”とした有機的な動きを表現します。
# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】
# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- 弾力ジェリーカーソル:速度に応じて伸び縮みする粘性カーソル -->
<div class="stage" data-jelly-root>
<div class="content">
<h1 class="title">ジェリーカーソル</h1>
<p class="lead">素早く動かすと、進行方向に“びよん”と伸びます。</p>
<div class="dots">
<span></span><span></span><span></span>
</div>
</div>
<!-- 弾力カーソル -->
<div class="jelly" data-jelly></div>
</div>
【CSS】
* { box-sizing: border-box; }
html, body { margin: 0; height: 100%; }
.stage {
position: relative;
height: 360px;
display: grid;
place-items: center;
overflow: hidden;
background:
radial-gradient(600px 360px at 30% 20%, #103b3a 0%, transparent 60%),
radial-gradient(600px 360px at 80% 90%, #2a1140 0%, transparent 60%),
#0a0d12;
color: #eafff8;
font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
cursor: none;
}
.content { text-align: center; padding: 24px; z-index: 1; }
.title {
margin: 0 0 10px;
font-size: clamp(30px, 6vw, 48px);
font-weight: 800;
letter-spacing: .03em;
background: linear-gradient(90deg, #6df0c2, #58c8ff);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.lead { margin: 0 0 22px; color: #9fc4bd; font-size: 14px; }
.dots { display: flex; gap: 14px; justify-content: center; }
.dots span {
width: 12px; height: 12px;
border-radius: 50%;
background: rgba(109,240,194,.5);
animation: pulse 1.8s ease-in-out infinite;
}
.dots span:nth-child(2) { animation-delay: .25s; }
.dots span:nth-child(3) { animation-delay: .5s; }
@keyframes pulse {
0%, 100% { transform: scale(1); opacity: .5; }
50% { transform: scale(1.5); opacity: 1; }
}
/* ジェリーカーソル本体:rotate+scaleで伸縮 */
.jelly {
position: fixed;
top: 0; left: 0;
width: 34px; height: 34px;
border-radius: 50%;
background: radial-gradient(circle at 35% 30%, #aeffe0, #2fd6a6 60%, #18b48a);
box-shadow: 0 0 20px rgba(47,214,166,.6);
pointer-events: none;
z-index: 9999;
will-change: transform;
opacity: 0; /* 初回移動まで非表示(中央の文字に重ならない) */
transition: opacity .3s ease;
}
[data-jelly-root].is-active .jelly { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
.dots span { animation: none; }
}
【JavaScript】
// 弾力ジェリーカーソル:速度ベクトルから伸び量と角度を求め、scale/rotateで変形
(() => {
const root = document.querySelector('[data-jelly-root]');
const jelly = document.querySelector('[data-jelly]');
if (!root || !jelly) return; // null安全
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
// 現在位置・目標位置・前フレーム位置
let x = window.innerWidth / 2, y = window.innerHeight / 2;
let tx = x, ty = y;
let px = x, py = y;
root.addEventListener('pointermove', (e) => {
tx = e.clientX;
ty = e.clientY;
if (!root.classList.contains('is-active')) root.classList.add('is-active');
});
root.addEventListener('pointerleave', () => root.classList.remove('is-active'));
const ease = reduce ? 1 : 0.2;
const tick = () => {
// 目標へ補間して滑らかに追従
x += (tx - x) * ease;
y += (ty - y) * ease;
// 速度(移動量)から伸びと回転を算出
const vx = x - px;
const vy = y - py;
px = x; py = y;
const speed = Math.min(Math.hypot(vx, vy), 80); // 上限でクランプ
const angle = Math.atan2(vy, vx) * (180 / Math.PI);
const stretch = reduce ? 0 : speed / 120; // 0〜0.66程度
// 進行方向に伸ばし、直交方向に縮める(体積保存風)
const sx = 1 + stretch;
const sy = 1 - stretch * 0.6;
jelly.style.transform =
`translate(${x}px, ${y}px) translate(-50%, -50%) ` +
`rotate(${angle}deg) scale(${sx}, ${sy})`;
requestAnimationFrame(tick);
};
requestAnimationFrame(tick);
})();
# 外部ライブラリ
なし(追加ライブラリ不要)
# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。