滑らかに動くカスタムカーソル(Mouse Follower)の作り方。JavaScriptで柔らかいマウス追従を実装する



Webサイトに Mouse Follower を取り入れると、ユーザーの視線を自然に誘導し、サイト内の回遊に「楽しさ」という付加価値を与えることができます。こうした演出は、ブランドを伝えるサイトにおいても非常に効果的な手法です。

マウスの動きに特定の要素を追従させることで、静的なページに動的なリズムが生まれ、クリックやホバーといったアクションをより直感的、かつ「柔らかく」感じさせることができます。ただ、実際に作ってみると「動きが硬くて安っぽく見える」「スマホでの挙動が不安定」「ページ読み込み時やリロード時に、円が表示されない」といった、細かな挙動が思い通りにいかない部分が出てきます。

こうした課題を解決するためには、座標を追いかけるだけでなく、ブラウザの描画負荷やメモリの状態を考慮した設計が必要になります。

ここでは、CSSとJavaScriptを組み合わせて、GPUによるスムーズな描画と、リロード・ページ遷移時のバグを排除した実務レベルの実装を解説します。

滑らかに動くMouse Followerの実装


まずは、HTML・CSS・JavaScriptの全体像を確認します。これらを組み合わせることで、リロードしても位置を保持し、滑らかに動くMouse Followerが完成します。

HTML

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <title>JavaScript | Mouse Follower</title>
  <meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0">
  <meta name="description" content="">
  <link rel="stylesheet" href="css/style.css">
</head>
<body>

  <div id="cursor"></div>

  <header>
    <nav></nav>
  </header>

  <main>

    <section id="hero">
      <h1>CREATIVE FLOW</h1>
      <p>滑らかな追従が、心地よいUXを生む。</p>
    </section>

  </main>

  <footer>
  </footer>

  <script src="js/common.js"></script>

</body>
</html>

CSS(style.css)

body {
  margin: 0;
  overflow: hidden;
  background-color: #f0f0f0;
  height: 100vh;
}

/* 追従する円のスタイル */
#cursor {
  position: fixed;
  top: 0;
  left: 0;
  width: 50px;
  height: 50px;
  background-color: rgba(255, 165, 0, 0.5); 
  border-radius: 50%;
  pointer-events: none;
  will-change: transform;
  z-index: 9999;
  opacity: 0; /* 初期状態は非表示にしておく */
  transition: opacity 0.3s ease; /* 現れる時にふわっとさせたい場合 */
}

/* サンプルコンテンツ:Hero Section */
#hero {
  height: 100vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  text-align: center;
  padding: 0 1.25rem;
  box-sizing: border-box;
  h1 {
    font-size: clamp(3rem, 10vw, 4rem);
    font-weight: bold;
    letter-spacing: 0.2rem;
    margin: 0 0 15px 0;
  }
  p {
    font-size: clamp(1rem, 3vw, 1.2rem);
    color: #a0a0a0;
    margin: 0;
    line-height: 1.6;
  }
}

JavaScript(common.js)

const cursor = document.getElementById('cursor');

// 前回の座標があれば読み込み、なければ 0 を初期値とする
// localStorage ではなく sessionStorage を使うことで、タブを閉じれば記録は消去される
let mouseX = parseFloat(sessionStorage.getItem('cursorX')) || 0;
let mouseY = parseFloat(sessionStorage.getItem('cursorY')) || 0;

// 円の現在地も保存された座標からスタートさせる
let currentX = mouseX;
let currentY = mouseY;

let isRunning = false;      // アニメーション実行フラグ
let isInitialized = false;  // 初回操作検知フラグ
const easing = 0.15;        // 追従の滑らかさ(15%ずつ接近)

// もし保存された座標があるなら、最初からその位置に表示しておく
if (sessionStorage.getItem('cursorX')) {
    isInitialized = true;
    cursor.style.opacity = "1";
    cursor.style.visibility = "visible";
    cursor.style.transform = `translate3d(${currentX - 25}px, ${currentY - 25}px, 0)`;
}

// --------------------------------------------------
// 滑らかな動きを作るメインループ
// --------------------------------------------------
const update = () => {
    const dx = mouseX - currentX;
    const dy = mouseY - currentY;

    // 距離が十分に縮まったら停止(CPU負荷軽減)
    if (Math.abs(dx) < 0.1 && Math.abs(dy) < 0.1) {
        currentX = mouseX;
        currentY = mouseY;
        isRunning = false;
        return;
    }

    currentX += dx * easing;
    currentY += dy * easing;

    // GPUを使用して描画。25pxは円の半径分(中心合わせ)
    cursor.style.transform = `translate3d(${currentX - 25}px, ${currentY - 25}px, 0)`;

    requestAnimationFrame(update);
};

// --------------------------------------------------
// 入力検知とブラウザ制限の回避
// --------------------------------------------------
const handleMove = (e) => {
    const clientX = e.touches ? e.touches[0].clientX : e.clientX;
    const clientY = e.touches ? e.touches[0].clientY : e.clientY;

    mouseX = clientX;
    mouseY = clientY;

    // 座標を保存(リロード時にここから再開できる)
    sessionStorage.setItem('cursorX', mouseX);
    sessionStorage.setItem('cursorY', mouseY);

    // リロード後、初めて操作した瞬間の処理
    if (!isInitialized) {
        // 保存データがない場合、現在地を最新のマウス位置に「ワープ」させる
        currentX = mouseX;
        currentY = mouseY;
        isInitialized = true;
        
        // 隠していた円を表示
        cursor.style.opacity = "1";
        cursor.style.visibility = "visible";
        
        // 初回の描画を即座に実行
        cursor.style.transform = `translate3d(${currentX - 25}px, ${currentY - 25}px, 0)`;
    }

    // アニメーションが止まっていれば再始動
    if (!isRunning) {
        isRunning = true;
        update();
    }
};

// --------------------------------------------------
// イベント登録
// --------------------------------------------------
window.addEventListener('mousemove', handleMove, { passive: true });
window.addEventListener('touchstart', handleMove, { passive: true });
window.addEventListener('touchmove', handleMove, { passive: true });


コード内のコメントでも触れていますが、実装のポイントを詳しく整理します。

HTMLで追従させる要素を構築


HTMLでは、追従させる要素を 直下に配置するのがポイントです。

<div id="cursor"></div>


この一行が、マウスに付いてくる円になります。メインコンテンツである <main><section> の外に置くことで、他の要素のレイアウト崩れに影響を与えず、常に最前面に表示させやすくしています。

CSSによる土台と外見の設定


CSSではデザインだけでなく、JavaScriptが計算した座標をブラウザがどう処理するかも設定していきます。

ページ全体の設定(body)


CSS

body {
  margin: 0;                /* ブラウザ標準の余白をゼロにする */
  overflow: hidden;         /* 画面からはみ出した要素によるスクロールを禁止する */
  background-color: #f0f0f0; /* 背景を薄いグレーにして、オレンジの円を見やすくする */
  height: 100vh;            /* 画面の高さをブラウザの表示領域いっぱいにする */
}


ブラウザが最初から持っている数ピクセルの余白を margin: 0 で消去することで、円が画面の端まで移動した際のがたつきを抑えます。また、追従する円が画面外に少しでもはみ出ると、ブラウザは「ページが広がった」と判断して一瞬だけスクロールバーを出そうとします。これが原因で画面全体が揺れるのを防ぐのが overflow: hidden の役割です。さらに、height: 100vh で画面の高さを表示領域いっぱいに固定することで、座標計算の基準を安定させています。

追従する円のスタイル設定(#cursor)


CSS

#cursor {
  position: fixed;          /* 画面上の絶対的な位置に固定する */
  top: 0;                   /* 配置の基準点を左上に設定 */
  left: 0;                  /* 配置の基準点を左上に設定 */
  width: 50px;              /* 円の横幅 */
  height: 50px;             /* 円の高さ */


position: fixed を使うのは、JavaScriptで取得するマウスの座標(clientX/clientY)が常に画面の左上を基準にするためです。円の配置も同じ基準に揃えることで、計算と表示を一致させます。幅と高さが50pxなのは、後の計算で半径25pxを差し引いて、マウスの先端を円の中心に合わせるための基準値となります。

CSS

  background-color: rgba(255, 165, 0, 0.5); 
  border-radius: 50%;       /* 角を丸めて正方形を円にする */
  pointer-events: none;     /* マウスの反応を無視させ、下のボタン等を押せるようにする */


rgba の最後の数値 0.5 で、背後にある文字や画像が隠れすぎないよう透過させます。
ここで実務上最も重要なのが pointer-events: none です。これがないと、円がマウスの先にある物理的な壁になってしまい、円の下にあるリンクやボタンをクリックできなくなる不具合が発生します。

CSS

  will-change: transform;   /* ブラウザに動くことを伝え、描画を高速化させる */
  z-index: 9999;            /* 他の要素よりも手前に表示させる */
  opacity: 0;               /* 最初は隠しておき、座標が決まってから出す */
  transition: opacity 0.3s ease; /* 現れる時に少し時間をかけて馴染ませる */
}


will-change: transform は、描画処理をGPU(グラフィックプロセッサ)に優先的に割り当てるための設定で、スクロールや移動時のカクつきを抑える効果があります。opacity: 0 は、マウスの位置が確定する前に円が画面左上に表示されてしまうのを防ぐための初期化設定です。

JavaScriptによる動きの制御


JavaScriptでは、「マウスの位置を監視する処理」と「円を滑らかに動かし続ける処理」を分けて管理します。

記憶の復元と初期位置の確定


まず、プログラムが起動した直後に、前回の記憶を呼び戻す工程から始まります。

JavaScript

const cursor = document.getElementById('cursor');

// 前回の座標があれば読み込み、なければ 0 を初期値とする
let mouseX = parseFloat(sessionStorage.getItem('cursorX')) || 0;
let mouseY = parseFloat(sessionStorage.getItem('cursorY')) || 0;

// 円の現在地も保存された座標からスタートさせる
let currentX = mouseX;
let currentY = mouseY;


ここでは、操作対象となる要素を取得した後、sessionStorage を使ってリロード前の最後の場所を確認しています。
通常のプログラムではリロードのたびに変数がリセットされ、円は画面左上(0,0)に戻ってしまいますが、このコードは一時メモリから値を引き出すことで、以前の場所を維持します。
目的地(マウス)となる mouseX だけでなく、円の現在地である currentX も同時にこの保存値で初期化しているのがポイントです。これにより、ページを読み込んだ瞬間に円が勝手に動き出すのを防ぎ、静止した状態でユーザーを待つことができます。

状態を制御するスイッチと追従の質感


次に、アニメーションの挙動をコントロールするための基本設定を行います。

JavaScript

let isRunning = false;      // アニメーション実行フラグ
let isInitialized = false;  // 初回操作検知フラグ
const easing = 0.15;        // 追従の滑らかさ(15%ずつ接近)



この部分では、プログラムの挙動を管理する3つの変数を定義しています。
isRunning は、アニメーションの計算ループが二重に起動するのを防ぐためのフラグです。これによって、無駄な処理によるブラウザの負荷増大を回避します。
isInitialized は、ユーザーがサイトを訪れてから一度でもマウス操作(またはタッチ操作)を行ったかを判別するためのものです。
そして、easing の 0.15 という数値は、追従の質感に直結するパラメータです。目的地(マウス)までの残り距離に対して「毎フレーム15%ずつ近づく」という処理により、動き出しは速く、停止直前はゆっくりと減速する滑らかな動きを実現しています。

ページ遷移直後の即時復元ロジック


保存されたデータがある場合、マウスを動かすのを待たずに円を表示させるための処理です。

JavaScript

// もし保存された座標があるなら、最初からその位置に表示しておく
if (sessionStorage.getItem('cursorX')) {
    isInitialized = true;
    cursor.style.opacity = "1";
    cursor.style.visibility = "visible";
    cursor.style.transform = `translate3d(${currentX - 25}px, ${currentY - 25}px, 0)`;
}


このブロックは、ページ遷移やリロードを挟んでも体験を途切れさせないための工夫です。メモリに座標が残っていれば、即座に初期化済みとして扱い、CSSで隠していた円を可視化します。
ここで translate3d を使用しているのは、PCのグラフィックチップ(GPU)に描画を任せるためです。従来の topleft を書き換える手法に比べ、圧倒的に負荷が低く、カクつきのない移動を実現します。また、半径分の 25px を引くことで、円の中心が正確にマウスの先端へ重なるように調整しています。

滑らかな追従と省エネ設計(update関数)


目的地(マウス)に向かって、円を徐々に近づけ続けるメインの計算処理です。

JavaScript

const update = () => {
    const dx = mouseX - currentX;
    const dy = mouseY - currentY;

    // 距離が十分に縮まったら停止(CPU負荷軽減)
    if (Math.abs(dx) < 0.1 && Math.abs(dy) < 0.1) {
        currentX = mouseX;
        currentY = mouseY;
        isRunning = false;
        return;
    }

    currentX += dx * easing;
    currentY += dy * easing;

    cursor.style.transform = `translate3d(${currentX - 25}px, ${currentY - 25}px, 0)`;
    requestAnimationFrame(update);
};


この関数の中では、目的地(マウス)と現在地(要素)の差(dx, dy)を常に監視しています。距離が 0.1px 未満まで縮まった際に計算ループを停止(return)させているのは、マウスが静止している間の不要な計算を省き、CPU負荷を最小限に抑えるためです。
移動中は requestAnimationFrame を使用し、ブラウザの描画更新タイミングに合わせて計算を繰り返します。これによって、デバイスの性能に応じた最適なフレームレートで、遅延のない滑らかな動きを実現しています。

入力検知と「ワープ」による初期化(handleMove関数)


ユーザーの操作をきっかけに、すべてを動かし始めるトリガーとなるセクションです。

JavaScript

const handleMove = (e) => {
    const clientX = e.touches ? e.touches[0].clientX : e.clientX;
    const clientY = e.touches ? e.touches[0].clientY : e.clientY;

    mouseX = clientX;
    mouseY = clientY;

    sessionStorage.setItem('cursorX', mouseX);
    sessionStorage.setItem('cursorY', mouseY);

    if (!isInitialized) {
        currentX = mouseX;
        currentY = mouseY;
        isInitialized = true;
        cursor.style.opacity = "1";
        cursor.style.visibility = "visible";
        cursor.style.transform = `translate3d(${currentX - 25}px, ${currentY - 25}px, 0)`;
    }

    if (!isRunning) {
        isRunning = true;
        update();
    }
};


ここではマウス操作だけでなく、スマホのタッチ操作(touches)も同時に受け取れるように設計しています。操作のたびに最新座標を sessionStorage へ記録し続けることで、ページをリロードした際も直前の位置から動きを再開できるようにしています。なお、sessionStorage に保存されたデータは、ブラウザのタブを閉じるまで保持されます。

重要なのは if (!isInitialized) の内部です。初期状態では円の出現位置が決まっていないため、ユーザーが初めて操作した瞬間に現在地を目的地へ一気に移動(ワープ)させてから表示を開始します。この処理により、読み込み直後に円が画面外から不自然に飛んでくる現象を防いでいます。

sessionStorage への記録状況は、ブラウザのデベロッパーツールの「Application」タブ内にある「Storage」項目から確認することができます。

ブラウザのデベロッパーツールでsessionStorageで管理するマウスの座標の確認する

イベントの登録


最後に、ブラウザに対して特定の操作を監視するためのイベントリスナーを登録します。

JavaScript

window.addEventListener('mousemove', handleMove, { passive: true });
window.addEventListener('touchstart', handleMove, { passive: true });
window.addEventListener('touchmove', handleMove, { passive: true });


ここでは「マウスが動いたとき」「画面に触れたとき」「指を滑らせたとき」のすべてにおいて、先ほどの handleMove を実行するように設定しています。
passive: true を指定することで、この処理がスクロールをブロックしないことをブラウザ側に明示し、実行時のカクつきを抑えています。

実際の動作と実装の流れ


ここまでのコードの組み立て方や、実際の滑らかな動き、デベロッパーツールでの sessionStorage の確認など、以下の動画で確認できます。
(動画:1分39秒)

  • 0:00 イントロダクション
  • 0:08 HTMLで追従要素を準備する
  • 0:17 CSSによる土台と外見のスタイル設定
  • 0:31 JavaScriptによるマウス追従の動きの制御
  • 0:56 JavaScriptによるMouse Followerの実装

まとめ:滑らかな追従を実現する実装のポイント


今回の実装では、マウスを追いかけるだけでなく、実務でのユーザー体験を損なわないための処理も重視しています。

まず動きの質感については、目的地(マウス)までの「残り距離の15%ずつ進む」というイージングの手法を採用しています。これにより、一定速度の機械的な移動ではなく、動き出しは速く、停止直前はゆっくりと収束する、柔らかい加減速が表現できます

パフォーマンス面では、will-change や translate3d を用いて描画処理を GPU へ逃がすだけでなく、円が目的地に到達して静止した瞬間に計算ループを停止させるフラグ管理を導入しています。これにより、デバイスへの負荷やバッテリー消費を最小限に抑えています。

さらに、Web制作で見落とされがちな初期化時の表示バグにも対策も講じています。初期状態では要素を非表示にしておき、JavaScriptが座標を捉えた瞬間に「ワープ」と「表示」を同時に行うことで、読み込み直後に円が画面左上から不自然に飛んでくる現象を抑えました。

見た目の演出だけでなく、こうした背後にある細かな処理の積み重ねが、サイト全体のクオリティを左右します。今回のロジックをベースに、プロジェクトに合わせて速度や透明度を調整してみてください。