画像トレイル

マウス移動の軌跡に沿って画像が次々に出現し、短時間でフェード消滅する Codrops 定番のイメージトレイル。移動量が閾値を超えるたびに次の画像を循環表示します。ポートフォリオやヒーローの没入演出に最適です。

#cursor#trail#image

ライブデモ

使用例(お題: カフェ MOON BREW)

この技法を「カフェ MOON BREW」というテーマのダミーサイトで実際に使った例です。

HTML
<!-- MOON BREW: カフェの世界観ギャラリー。マウス軌跡に店内写真が湧き出す演出 -->
<div class="trail-stage" data-trail-root>
  <header class="cafe-bar">
    <span class="cafe-logo"><span class="cafe-cup">☕</span>MOON BREW</span>
    <span class="cafe-sub">GALLERY</span>
  </header>
  <div class="caption">
    <h1 class="title">月夜のカフェへ、<br>ようこそ。</h1>
    <p class="lead">画面をなぞると、店内の風景がふわりと現れます。</p>
  </div>
  <!-- 画像はJSがプールとして生成・循環表示 -->
</div>
CSS
/* MOON BREW カフェギャラリー: クリーム地に店内写真がトレイルで湧き出す */
* { box-sizing: border-box; }
/* iframe全面を塗る(一覧でも白抜けしない) */
html, body {
  margin: 0;
  width: 100%;
  min-height: 100%;
  background: #f5ede1;
}

.trail-stage {
  position: relative;
  width: 100%;
  min-height: 360px;
  height: 100vh;
  max-height: 100%;
  overflow: hidden;
  background:
    radial-gradient(700px 420px at 50% 38%, #fbf4e8, #f0e4d2),
    #f5ede1;
  font-family: "Hiragino Sans", "Yu Gothic", system-ui, sans-serif;
  color: #2b1d12;
  cursor: crosshair;
}

/* ヘッダー */
.cafe-bar {
  position: absolute;
  top: 0; left: 0; right: 0;
  z-index: 2;
  display: flex;
  align-items: baseline;
  gap: 14px;
  padding: 15px 26px;
  pointer-events: none;
}
.cafe-logo {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-weight: 800;
  font-size: 16px;
  letter-spacing: .04em;
}
.cafe-cup { font-size: 18px; }
.cafe-sub {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .24em;
  color: #b89a76;
}

/* 中央キャプション(画像より下のレイヤー) */
.caption {
  position: absolute;
  inset: 0;
  z-index: 0;
  display: grid;
  place-items: center;
  text-align: center;
  padding: 24px;
  color: #2b1d12;
  pointer-events: none;
  user-select: none;
}
.caption .title {
  margin: 0 0 12px;
  font-size: clamp(26px, 5.5vw, 40px);
  font-weight: 900;
  letter-spacing: .04em;
  line-height: 1.3;
}
.caption .lead { margin: 0; font-size: 13px; color: #6b5640; }

/* トレイル画像: JSが座標を設定し、追加クラスでアニメ */
.trail-img {
  position: absolute;
  top: 0; left: 0;
  width: 120px;
  height: 84px;
  margin: -42px 0 0 -60px; /* 中心基準に配置 */
  object-fit: cover;
  border-radius: 10px;
  box-shadow: 0 12px 30px rgba(43,29,18,.3);
  pointer-events: none;
  opacity: 0;
  will-change: transform, opacity;
  z-index: 1;
  border: 3px solid #fff;
  transform: translate3d(var(--x), var(--y), 0) scale(.6) rotate(var(--rot));
}

/* 出現アニメ: ぽんと現れてゆっくりフェード消滅 */
.trail-img.show {
  animation: trailPop var(--life, 900ms) cubic-bezier(.2, .8, .2, 1) forwards;
}

@keyframes trailPop {
  0%   { opacity: 0; transform: translate3d(var(--x), var(--y), 0) scale(.6) rotate(var(--rot)); }
  18%  { opacity: 1; transform: translate3d(var(--x), var(--y), 0) scale(1) rotate(0deg); }
  100% { opacity: 0; transform: translate3d(var(--x), calc(var(--y) + 26px), 0) scale(.92) rotate(0deg); }
}

@media (prefers-reduced-motion: reduce) {
  .trail-img.show { animation: trailFade var(--life, 700ms) linear forwards; }
  @keyframes trailFade {
    0%   { opacity: 0; transform: translate3d(var(--x), var(--y), 0) scale(1) rotate(0deg); }
    20%  { opacity: .9; }
    100% { opacity: 0; transform: translate3d(var(--x), var(--y), 0) scale(1) rotate(0deg); }
  }
}
JavaScript
// MOON BREW: 軌跡に店内写真が湧き出す画像トレイル。待機中は自動巡回、操作で本物に追従
(() => {
  const root = document.querySelector('[data-trail-root]');
  if (!root) return; // null安全

  const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

  // カフェ風の写真URL(循環使用)
  const SRCS = [201, 225, 240, 292, 312, 326, 365, 431].map(
    (n) => `https://picsum.photos/240/168?random=${n}`
  );

  // 画像要素のプールを事前生成
  const POOL = SRCS.length;
  const pool = [];
  for (let i = 0; i < POOL; i++) {
    const img = document.createElement('img');
    img.className = 'trail-img';
    img.src = SRCS[i];
    img.alt = '';
    img.loading = 'eager';
    img.decoding = 'async';
    img.draggable = false;
    root.appendChild(img);
    pool.push(img);
  }

  let index = 0; // 次に使う画像(循環)

  // 1枚出現させる
  const spawn = (x, y) => {
    const img = pool[index % POOL];
    index++;
    const life = reduce ? 700 : 760 + Math.random() * 380;
    const rot = (Math.random() * 16 - 8).toFixed(1);
    img.style.setProperty('--x', `${x}px`);
    img.style.setProperty('--y', `${y}px`);
    img.style.setProperty('--rot', `${rot}deg`);
    img.style.setProperty('--life', `${life}ms`);
    // アニメ再起動
    img.classList.remove('show');
    void img.offsetWidth; // 強制リフロー
    img.classList.add('show');
  };

  // reduced-motion: 中央付近に静的に数枚並べてデモ内容を提示
  if (reduce) {
    const place = () => {
      const r = root.getBoundingClientRect();
      const cx = r.width / 2, cy = r.height / 2;
      const offs = [[-130, -10], [0, 12], [130, -6]];
      offs.forEach((o, i) => {
        const img = pool[i % POOL];
        img.style.setProperty('--x', `${cx + o[0]}px`);
        img.style.setProperty('--y', `${cy + o[1]}px`);
        img.style.setProperty('--rot', '0deg');
        img.style.opacity = '1';
        img.style.transform =
          `translate3d(${cx + o[0]}px, ${cy + o[1]}px, 0) scale(1) rotate(0deg)`;
      });
    };
    place();
    return;
  }

  const THRESHOLD = 48; // 移動量しきい値

  let last = null;
  let dist = 0;
  let usePointer = false;
  let lastMove = 0;
  const IDLE = 1600;

  // 移動量に応じて画像を出す
  const advance = (x, y) => {
    if (last) {
      dist += Math.hypot(x - last.x, y - last.y);
      while (dist >= THRESHOLD) {
        spawn(x, y);
        dist -= THRESHOLD;
      }
    }
    last = { x, y };
  };

  // 仮想カーソルの自動経路(リサージュ)
  const autoPos = (t) => {
    const r = root.getBoundingClientRect();
    const cx = r.width / 2, cy = r.height / 2;
    const ax = r.width * 0.30, ay = r.height * 0.26;
    return {
      x: cx + Math.sin(t * 0.00060) * ax,
      y: cy + Math.sin(t * 0.00097 + 1.1) * ay,
    };
  };

  root.addEventListener('pointermove', (e) => {
    const r = root.getBoundingClientRect();
    const x = Math.min(Math.max(e.clientX - r.left, 0), r.width);
    const y = Math.min(Math.max(e.clientY - r.top, 0), r.height);
    if (!usePointer) { usePointer = true; last = null; dist = 0; }
    lastMove = performance.now();
    advance(x, y);
  });

  const loop = (now) => {
    if (usePointer && now - lastMove > IDLE) { usePointer = false; last = null; dist = 0; }
    if (!usePointer) {
      const p = autoPos(now);
      advance(p.x, p.y);
    }
    requestAnimationFrame(loop);
  };
  requestAnimationFrame(loop);
})();

コード

HTML
<!-- 画像トレイル:マウス軌跡に沿って画像が出現し短時間でフェード消滅 -->
<div class="trail-stage" data-trail-root>
  <div class="caption">
    <h1 class="title">IMAGE&nbsp;TRAIL</h1>
    <p class="lead">マウスを動かすと、軌跡に画像が湧き出します。</p>
  </div>
  <!-- 画像はJSがプールとして生成・循環表示する -->
</div>
CSS
* { box-sizing: border-box; }
/* iframe全面を暗く塗る(一覧でも白抜けしない) */
html, body {
  margin: 0;
  width: 100%;
  min-height: 100%;
  background: #0a0b12;
}

.trail-stage {
  position: relative;
  width: 100%;
  min-height: 360px;
  height: 100vh;
  max-height: 100%;
  overflow: hidden;
  background:
    radial-gradient(700px 420px at 50% 40%, #1c1f33, #0a0b12);
  font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
  cursor: crosshair;
}

/* 中央キャプション(画像より下のレイヤー) */
.caption {
  position: absolute;
  inset: 0;
  z-index: 0;
  display: grid;
  place-items: center;
  text-align: center;
  padding: 24px;
  color: #eef1ff;
  pointer-events: none;
  user-select: none;
}
.caption .title {
  margin: 0 0 10px;
  font-size: clamp(30px, 6.5vw, 52px);
  font-weight: 900;
  letter-spacing: .1em;
}
.caption .lead { margin: 0; font-size: 14px; color: rgba(238,241,255,.7); }

/* トレイル画像:JSが座標を設定し、追加クラスでアニメ */
.trail-img {
  position: absolute;
  top: 0; left: 0;
  width: 120px;
  height: 84px;
  margin: -42px 0 0 -60px; /* 中心基準に配置 */
  object-fit: cover;
  border-radius: 10px;
  box-shadow: 0 10px 30px rgba(0,0,0,.45);
  pointer-events: none;
  opacity: 0;
  will-change: transform, opacity;
  z-index: 1;
  /* CSS変数で出現位置と初期回転を受け取る */
  transform: translate3d(var(--x), var(--y), 0) scale(.6) rotate(var(--rot));
}

/* 出現アニメ:ぽんと現れてゆっくりフェード消滅 */
.trail-img.show {
  animation: trailPop var(--life, 900ms) cubic-bezier(.2, .8, .2, 1) forwards;
}

@keyframes trailPop {
  0%   { opacity: 0; transform: translate3d(var(--x), var(--y), 0) scale(.6) rotate(var(--rot)); }
  18%  { opacity: 1; transform: translate3d(var(--x), var(--y), 0) scale(1) rotate(0deg); }
  100% { opacity: 0; transform: translate3d(var(--x), calc(var(--y) + 26px), 0) scale(.92) rotate(0deg); }
}

/* モーション控えめ:アニメせず一瞬だけ薄く表示してすぐ消す */
@media (prefers-reduced-motion: reduce) {
  .trail-img.show { animation: trailFade var(--life, 700ms) linear forwards; }
  @keyframes trailFade {
    0%   { opacity: 0; transform: translate3d(var(--x), var(--y), 0) scale(1) rotate(0deg); }
    20%  { opacity: .9; }
    100% { opacity: 0; transform: translate3d(var(--x), var(--y), 0) scale(1) rotate(0deg); }
  }
}
JavaScript
// 画像トレイル:仮想カーソルの自動軌跡に沿って画像を出現させ、操作時は本物に追従
(() => {
  const root = document.querySelector('[data-trail-root]');
  if (!root) return; // null安全

  const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

  // picsumの複数画像URL(循環使用)
  const SRCS = [101, 102, 103, 104, 106, 108, 110, 112].map(
    (n) => `https://picsum.photos/240/168?random=${n}`
  );

  // 画像要素のプールを事前生成(DOM生成コストを抑える)
  const POOL = SRCS.length;
  const pool = [];
  for (let i = 0; i < POOL; i++) {
    const img = document.createElement('img');
    img.className = 'trail-img';
    img.src = SRCS[i];
    img.alt = '';
    img.loading = 'eager';
    img.decoding = 'async';
    img.draggable = false;
    root.appendChild(img);
    pool.push(img);
  }

  let index = 0; // 次に使う画像(循環)

  // 1枚出現させる
  const spawn = (x, y) => {
    const img = pool[index % POOL];
    index++;

    // ランダムな寿命と初期回転で単調さを回避
    const life = reduce ? 700 : 760 + Math.random() * 380;
    const rot = (Math.random() * 16 - 8).toFixed(1);

    img.style.setProperty('--x', `${x}px`);
    img.style.setProperty('--y', `${y}px`);
    img.style.setProperty('--rot', `${rot}deg`);
    img.style.setProperty('--life', `${life}ms`);

    // アニメ再起動:一旦クラスを外し、リフロー後に付け直す
    img.classList.remove('show');
    void img.offsetWidth; // 強制リフロー
    img.classList.add('show');
  };

  // reduced-motion:自動巡回せず中央付近に静的に数枚並べてデモ内容を提示
  if (reduce) {
    const place = () => {
      const r = root.getBoundingClientRect();
      const cx = r.width / 2, cy = r.height / 2;
      const offs = [[-130, -10], [0, 12], [130, -6]];
      offs.forEach((o, i) => {
        const img = pool[i % POOL];
        img.style.setProperty('--x', `${cx + o[0]}px`);
        img.style.setProperty('--y', `${cy + o[1]}px`);
        img.style.setProperty('--rot', '0deg');
        img.style.opacity = '1';
        img.style.transform =
          `translate3d(${cx + o[0]}px, ${cy + o[1]}px, 0) scale(1) rotate(0deg)`;
      });
    };
    place();
    return;
  }

  const THRESHOLD = 48; // 移動量しきい値:これを超えたら次の画像を出す

  let last = null;        // 直近の座標(本物/仮想共通)
  let dist = 0;           // 累積移動量
  let usePointer = false; // 本物のポインタ追従中か
  let lastMove = 0;       // 最後にポインタが動いた時刻
  let raf = 0;
  const IDLE = 1600;      // 無操作でこの時間が過ぎたら自動巡回へ戻る(ms)

  // 移動した分だけ画像を出す(速い動きでも取りこぼさない)
  const advance = (x, y) => {
    if (last) {
      dist += Math.hypot(x - last.x, y - last.y);
      while (dist >= THRESHOLD) {
        spawn(x, y);
        dist -= THRESHOLD;
      }
    }
    last = { x, y };
  };

  // 仮想カーソルの自動経路(リサージュ):軌跡に沿って画像が湧き出す
  const autoPos = (t) => {
    const r = root.getBoundingClientRect();
    const cx = r.width / 2, cy = r.height / 2;
    const ax = r.width * 0.30, ay = r.height * 0.26;
    return {
      x: cx + Math.sin(t * 0.00060) * ax,
      y: cy + Math.sin(t * 0.00097 + 1.1) * ay,
    };
  };

  // pointermoveで本物の座標を採用
  root.addEventListener('pointermove', (e) => {
    const r = root.getBoundingClientRect();
    const x = Math.min(Math.max(e.clientX - r.left, 0), r.width);
    const y = Math.min(Math.max(e.clientY - r.top, 0), r.height);
    if (!usePointer) { usePointer = true; last = null; dist = 0; } // 切替時は累積リセット
    lastMove = performance.now();
    advance(x, y);
  });

  // メインループ:アイドル中は自動軌跡で画像を出す
  const loop = (now) => {
    if (usePointer && now - lastMove > IDLE) { usePointer = false; last = null; dist = 0; }
    if (!usePointer) {
      const p = autoPos(now);
      advance(p.x, p.y);
    }
    raf = requestAnimationFrame(loop);
  };
  raf = requestAnimationFrame(loop);
})();

🤖 AIエージェント用プロンプト

このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「画像トレイル」の効果を追加してください。

# 追加してほしい効果
画像トレイル(カスタムカーソル)
マウス移動の軌跡に沿って画像が次々に出現し、短時間でフェード消滅する Codrops 定番のイメージトレイル。移動量が閾値を超えるたびに次の画像を循環表示します。ポートフォリオやヒーローの没入演出に最適です。

# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- 画像トレイル:マウス軌跡に沿って画像が出現し短時間でフェード消滅 -->
<div class="trail-stage" data-trail-root>
  <div class="caption">
    <h1 class="title">IMAGE&nbsp;TRAIL</h1>
    <p class="lead">マウスを動かすと、軌跡に画像が湧き出します。</p>
  </div>
  <!-- 画像はJSがプールとして生成・循環表示する -->
</div>

【CSS】
* { box-sizing: border-box; }
/* iframe全面を暗く塗る(一覧でも白抜けしない) */
html, body {
  margin: 0;
  width: 100%;
  min-height: 100%;
  background: #0a0b12;
}

.trail-stage {
  position: relative;
  width: 100%;
  min-height: 360px;
  height: 100vh;
  max-height: 100%;
  overflow: hidden;
  background:
    radial-gradient(700px 420px at 50% 40%, #1c1f33, #0a0b12);
  font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
  cursor: crosshair;
}

/* 中央キャプション(画像より下のレイヤー) */
.caption {
  position: absolute;
  inset: 0;
  z-index: 0;
  display: grid;
  place-items: center;
  text-align: center;
  padding: 24px;
  color: #eef1ff;
  pointer-events: none;
  user-select: none;
}
.caption .title {
  margin: 0 0 10px;
  font-size: clamp(30px, 6.5vw, 52px);
  font-weight: 900;
  letter-spacing: .1em;
}
.caption .lead { margin: 0; font-size: 14px; color: rgba(238,241,255,.7); }

/* トレイル画像:JSが座標を設定し、追加クラスでアニメ */
.trail-img {
  position: absolute;
  top: 0; left: 0;
  width: 120px;
  height: 84px;
  margin: -42px 0 0 -60px; /* 中心基準に配置 */
  object-fit: cover;
  border-radius: 10px;
  box-shadow: 0 10px 30px rgba(0,0,0,.45);
  pointer-events: none;
  opacity: 0;
  will-change: transform, opacity;
  z-index: 1;
  /* CSS変数で出現位置と初期回転を受け取る */
  transform: translate3d(var(--x), var(--y), 0) scale(.6) rotate(var(--rot));
}

/* 出現アニメ:ぽんと現れてゆっくりフェード消滅 */
.trail-img.show {
  animation: trailPop var(--life, 900ms) cubic-bezier(.2, .8, .2, 1) forwards;
}

@keyframes trailPop {
  0%   { opacity: 0; transform: translate3d(var(--x), var(--y), 0) scale(.6) rotate(var(--rot)); }
  18%  { opacity: 1; transform: translate3d(var(--x), var(--y), 0) scale(1) rotate(0deg); }
  100% { opacity: 0; transform: translate3d(var(--x), calc(var(--y) + 26px), 0) scale(.92) rotate(0deg); }
}

/* モーション控えめ:アニメせず一瞬だけ薄く表示してすぐ消す */
@media (prefers-reduced-motion: reduce) {
  .trail-img.show { animation: trailFade var(--life, 700ms) linear forwards; }
  @keyframes trailFade {
    0%   { opacity: 0; transform: translate3d(var(--x), var(--y), 0) scale(1) rotate(0deg); }
    20%  { opacity: .9; }
    100% { opacity: 0; transform: translate3d(var(--x), var(--y), 0) scale(1) rotate(0deg); }
  }
}

【JavaScript】
// 画像トレイル:仮想カーソルの自動軌跡に沿って画像を出現させ、操作時は本物に追従
(() => {
  const root = document.querySelector('[data-trail-root]');
  if (!root) return; // null安全

  const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

  // picsumの複数画像URL(循環使用)
  const SRCS = [101, 102, 103, 104, 106, 108, 110, 112].map(
    (n) => `https://picsum.photos/240/168?random=${n}`
  );

  // 画像要素のプールを事前生成(DOM生成コストを抑える)
  const POOL = SRCS.length;
  const pool = [];
  for (let i = 0; i < POOL; i++) {
    const img = document.createElement('img');
    img.className = 'trail-img';
    img.src = SRCS[i];
    img.alt = '';
    img.loading = 'eager';
    img.decoding = 'async';
    img.draggable = false;
    root.appendChild(img);
    pool.push(img);
  }

  let index = 0; // 次に使う画像(循環)

  // 1枚出現させる
  const spawn = (x, y) => {
    const img = pool[index % POOL];
    index++;

    // ランダムな寿命と初期回転で単調さを回避
    const life = reduce ? 700 : 760 + Math.random() * 380;
    const rot = (Math.random() * 16 - 8).toFixed(1);

    img.style.setProperty('--x', `${x}px`);
    img.style.setProperty('--y', `${y}px`);
    img.style.setProperty('--rot', `${rot}deg`);
    img.style.setProperty('--life', `${life}ms`);

    // アニメ再起動:一旦クラスを外し、リフロー後に付け直す
    img.classList.remove('show');
    void img.offsetWidth; // 強制リフロー
    img.classList.add('show');
  };

  // reduced-motion:自動巡回せず中央付近に静的に数枚並べてデモ内容を提示
  if (reduce) {
    const place = () => {
      const r = root.getBoundingClientRect();
      const cx = r.width / 2, cy = r.height / 2;
      const offs = [[-130, -10], [0, 12], [130, -6]];
      offs.forEach((o, i) => {
        const img = pool[i % POOL];
        img.style.setProperty('--x', `${cx + o[0]}px`);
        img.style.setProperty('--y', `${cy + o[1]}px`);
        img.style.setProperty('--rot', '0deg');
        img.style.opacity = '1';
        img.style.transform =
          `translate3d(${cx + o[0]}px, ${cy + o[1]}px, 0) scale(1) rotate(0deg)`;
      });
    };
    place();
    return;
  }

  const THRESHOLD = 48; // 移動量しきい値:これを超えたら次の画像を出す

  let last = null;        // 直近の座標(本物/仮想共通)
  let dist = 0;           // 累積移動量
  let usePointer = false; // 本物のポインタ追従中か
  let lastMove = 0;       // 最後にポインタが動いた時刻
  let raf = 0;
  const IDLE = 1600;      // 無操作でこの時間が過ぎたら自動巡回へ戻る(ms)

  // 移動した分だけ画像を出す(速い動きでも取りこぼさない)
  const advance = (x, y) => {
    if (last) {
      dist += Math.hypot(x - last.x, y - last.y);
      while (dist >= THRESHOLD) {
        spawn(x, y);
        dist -= THRESHOLD;
      }
    }
    last = { x, y };
  };

  // 仮想カーソルの自動経路(リサージュ):軌跡に沿って画像が湧き出す
  const autoPos = (t) => {
    const r = root.getBoundingClientRect();
    const cx = r.width / 2, cy = r.height / 2;
    const ax = r.width * 0.30, ay = r.height * 0.26;
    return {
      x: cx + Math.sin(t * 0.00060) * ax,
      y: cy + Math.sin(t * 0.00097 + 1.1) * ay,
    };
  };

  // pointermoveで本物の座標を採用
  root.addEventListener('pointermove', (e) => {
    const r = root.getBoundingClientRect();
    const x = Math.min(Math.max(e.clientX - r.left, 0), r.width);
    const y = Math.min(Math.max(e.clientY - r.top, 0), r.height);
    if (!usePointer) { usePointer = true; last = null; dist = 0; } // 切替時は累積リセット
    lastMove = performance.now();
    advance(x, y);
  });

  // メインループ:アイドル中は自動軌跡で画像を出す
  const loop = (now) => {
    if (usePointer && now - lastMove > IDLE) { usePointer = false; last = null; dist = 0; }
    if (!usePointer) {
      const p = autoPos(now);
      advance(p.x, p.y);
    }
    raf = requestAnimationFrame(loop);
  };
  raf = requestAnimationFrame(loop);
})();

# 外部ライブラリ
なし(追加ライブラリ不要)

# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。