スクロール+マウス連動UIの作り方【スマホでも破綻しない設計】

Scroll mouse interaction uiのアイキャッチ画像

スクロールに合わせて要素が少し動くだけでも、UIの印象はかなり変わります。
さらにPCではマウス位置に合わせてわずかに動きを足すと、画面に奥行きや気持ちよさが出ます。

ただし、ここで注意したいのがマウス前提の設計にしないことです。

スマホではマウスイベントが使えないため、mousemove がないと成立しないUIにしてしまうと、PCでは動くのにスマホでは何も起きない状態になります。これでは演出ではなく、ただの不親切なUIになってしまいます。

この記事では、スクロールだけで成立するUIをベースにして、PCだけマウス演出を追加する方法を紹介します。
「スマホでも破綻しない」「でもPCだとちょっと気持ちいい」そんなバランスの良い実装です。

この記事で扱う内容

  • スクロールを主役にしたUIアニメーション
  • PCではマウスで少しリッチに見せる
  • スマホではスクロールだけで成立させる
  • transform を使って軽く動かす
  • マウス依存UIにしない考え方

スクロール量に応じて要素を動かす基本形は、先に Scroll Trigger AnimationParallax Scroll Animation を見ておくと入りやすいです。

1.スクロール+マウス連動UIとは?

スクロール+マウス連動UIは、次の2つを組み合わせた表現です。

  1. スクロール量に応じて要素を動かす
  2. マウス位置に応じて要素を少しだけ補正する

たとえばカードや背景の円、装飾パーツなどを少しずつズラすと、平面的なレイアウトでも立体感のある見え方になります。

ただし重要なのは、主役はスクロールであることです。
マウスはあくまで補助演出にしておくと、スマホでも自然に成立します。

2.まずは設計の考え方

このUIは、次の順番で考えると破綻しにくいです。

①まずスクロールだけで成立させる

最初に、スクロール量だけでアニメーションが成立するように作ります。
この時点でスマホでもPCでも見せたい体験が完成している状態が理想です。

②その上でPCだけマウス演出を足す

次に、ポインタが細かく使える端末だけでマウス演出を追加します。
これなら、PCでは少しリッチに、スマホではシンプルに見せられます。

③動かしすぎない

マウス追従を大きくしすぎると、UIより演出が前に出て読みにくくなります。
数px〜十数px程度の小さな補正に留めるのがコツです。

スクロール中の見せ方を固定レイアウト込みで考えたい場合は、Sticky Scroll AnimationScroll Story UI もつながります。

3.まずはシンプル版(スクロールだけで動かす)

まずは、スクロールだけで要素を動かすシンプルな例を作ります。
ここではマウスは使わず、スクロール量に応じてボックスが少しだけ動くようにします。

この段階で「スクロール連動の基本」が理解できればOKです。

HTML

<div class="simple-area">
  <div class="simple-box">Scroll</div>
</div>

CSS

.simple-area {
  height: 200vh;
  display: flex;
  align-items: center;
  justify-content: center;
}

.simple-box {
  width: 120px;
  height: 120px;
  background: #4cc9f0;
  display: flex;
  align-items: center;
  justify-content: center;
  color: #0b1720;
  font-weight: bold;
  border-radius: 12px;
}

JavaScript

const box = document.querySelector(".simple-box");

window.addEventListener("scroll", () => {
  const scrollY = window.scrollY;

  // 少しだけ動かす
  const moveY = scrollY * 0.2;

  box.style.transform = `translateY(${moveY}px)`;
});

スクロール量に倍率をかけるだけでも、簡単に動きが作れます。
この「scroll × 係数」の考え方が、ほとんどのスクロールUIの基本になります。

4.マウスで少しだけ動きを足す

次に、PCだけでマウス位置に応じて少しだけ動きを足します。
ただし、スマホではマウスが使えないため、ポインタがある環境だけで処理を有効にします。

JS(追加版)

const box = document.querySelector(".simple-box");

const hasMouse = window.matchMedia("(pointer: fine)").matches;

let mouseX = 0;

if (hasMouse) {
  window.addEventListener("mousemove", (e) => {
    const center = window.innerWidth / 2;
    mouseX = (e.clientX - center) * 0.05;
  });
}

window.addEventListener("scroll", () => {
  const scrollY = window.scrollY;

  const moveY = scrollY * 0.2;

  box.style.transform = `translate(${mouseX}px, ${moveY}px)`;
});

マウスの動きはあくまで補助として加えています。
スクロールだけでも成立する状態をベースにすることで、スマホでも自然に動作するUIになります。

5.完成版

HTML

<section class="hero">
  <div class="hero__bg hero__bg--1" data-depth="12"></div>
  <div class="hero__bg hero__bg--2" data-depth="20"></div>

  <div class="hero__content" data-depth="8">
    <p class="eyebrow">Scroll + Mouse Interaction</p>
    <h1>Scroll first.<br>Mouse second.</h1>
    <p class="lead">
      スクロールを主役にして、PCではマウスで少しだけ奥行きを足すUIです。
    </p>
  </div>

  <div class="hero__card hero__card--left" data-depth="16">
    <span>Layer A</span>
  </div>

  <div class="hero__card hero__card--right" data-depth="24">
    <span>Layer B</span>
  </div>
</section>

<section class="dummy">
  <div class="dummy__inner">
    <h2>Scroll down</h2>
    <p>
      下へスクロールすると全体がゆるく動き、PCではマウス位置でも少しだけ補正されます。
      スマホではスクロールだけで自然に成立します。
    </p>
  </div>
</section>

CSS

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  font-family: Arial, sans-serif;
  color: #e8eef2;
  background:
    radial-gradient(circle at top, #173042 0%, #0b1720 45%, #050a0e 100%);
  overflow-x: hidden;
}

.hero {
  position: relative;
  min-height: 100vh;
  padding: 120px 24px 80px;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}

.hero__content,
.hero__card,
.hero__bg {
  will-change: transform;
}

.hero__content {
  position: relative;
  z-index: 3;
  max-width: 720px;
  text-align: center;
}

.eyebrow {
  margin: 0 0 16px;
  font-size: 13px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  opacity: 0.7;
}

.hero h1 {
  margin: 0;
  font-size: clamp(40px, 8vw, 88px);
  line-height: 0.98;
  letter-spacing: -0.03em;
}

.lead {
  margin: 20px auto 0;
  max-width: 560px;
  font-size: clamp(15px, 2vw, 18px);
  line-height: 1.8;
  opacity: 0.82;
}

.hero__bg {
  position: absolute;
  border-radius: 50%;
  filter: blur(6px);
  opacity: 0.55;
  z-index: 1;
}

.hero__bg--1 {
  width: 320px;
  height: 320px;
  left: 8%;
  top: 16%;
  background: linear-gradient(135deg, #66d1ff, #246bff);
}

.hero__bg--2 {
  width: 260px;
  height: 260px;
  right: 10%;
  bottom: 14%;
  background: linear-gradient(135deg, #7cf0c7, #247f8e);
}

.hero__card {
  position: absolute;
  width: 180px;
  height: 120px;
  border-radius: 20px;
  display: grid;
  place-items: center;
  backdrop-filter: blur(10px);
  background: rgba(255, 255, 255, 0.08);
  border: 1px solid rgba(255, 255, 255, 0.12);
  box-shadow: 0 20px 50px rgba(0, 0, 0, 0.28);
  z-index: 2;
}

.hero__card--left {
  left: 10%;
  bottom: 18%;
  transform: rotate(-8deg);
}

.hero__card--right {
  right: 12%;
  top: 20%;
  transform: rotate(8deg);
}

.hero__card span {
  font-size: 14px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  opacity: 0.85;
}

.dummy {
  min-height: 100vh;
  padding: 120px 24px;
}

.dummy__inner {
  max-width: 760px;
  margin: 0 auto;
}

.dummy h2 {
  font-size: clamp(28px, 5vw, 48px);
  margin: 0 0 20px;
}

.dummy p {
  margin: 0;
  font-size: 16px;
  line-height: 1.9;
  opacity: 0.82;
}

@media (max-width: 768px) {
  .hero {
    padding-top: 96px;
  }

  .hero__card {
    width: 140px;
    height: 92px;
  }

  .hero__card--left {
    left: 4%;
    bottom: 14%;
  }

  .hero__card--right {
    right: 4%;
    top: 16%;
  }

  .hero__bg--1 {
    width: 220px;
    height: 220px;
  }

  .hero__bg--2 {
    width: 180px;
    height: 180px;
  }
}

JavaScript

const layers = document.querySelectorAll("[data-depth]");
const hasFinePointer = window.matchMedia("(pointer: fine)").matches;

let mouseX = 0;
let mouseY = 0;
let currentMouseX = 0;
let currentMouseY = 0;

function clamp(value, min, max) {
  return Math.min(Math.max(value, min), max);
}

function updateMousePosition(x, y) {
  const nx = (x / window.innerWidth) * 2 - 1;
  const ny = (y / window.innerHeight) * 2 - 1;

  mouseX = clamp(nx, -1, 1);
  mouseY = clamp(ny, -1, 1);
}

if (hasFinePointer) {
  window.addEventListener("mousemove", (e) => {
    updateMousePosition(e.clientX, e.clientY);
  });
}

function animate() {
  const scrollY = window.scrollY;
  const scrollProgress = clamp(scrollY / window.innerHeight, 0, 1);

  currentMouseX += (mouseX - currentMouseX) * 0.08;
  currentMouseY += (mouseY - currentMouseY) * 0.08;

  layers.forEach((layer) => {
    const depth = Number(layer.dataset.depth);

    const scrollOffsetY = scrollProgress * depth * -2.2;
    const mouseOffsetX = hasFinePointer ? currentMouseX * depth : 0;
    const mouseOffsetY = hasFinePointer ? currentMouseY * depth : 0;

    let rotate = "";

    if (layer.classList.contains("hero__card--left")) {
      rotate = " rotate(-8deg)";
    }

    if (layer.classList.contains("hero__card--right")) {
      rotate = " rotate(8deg)";
    }

    layer.style.transform =
      `translate3d(${mouseOffsetX}px, ${scrollOffsetY + mouseOffsetY}px, 0)` + rotate;
  });

  requestAnimationFrame(animate);
}

animate();

6.この実装のポイント

スクロールを主役にしている

今回の実装では、スクロール量から scrollProgress を作り、それを各レイヤーの移動量に反映しています。
これによって、マウスが使えないスマホでも見た目が成立します。

マウスは補正として足している

(pointer: fine) でPC系のポインタ環境だけを判定し、マウス入力が使える時だけ補正を入れています。
これならスマホでは無理に追従を入れず、自然にスキップできます。

transform で動かしている

top や left を直接変えるのではなく、transform: translate3d() を使って動かしています。
アニメーションではこの方が軽く、見た目も安定しやすいです。

少し遅れて追従させている

マウス座標をそのまま使うのではなく、現在値に少しずつ近づけています。
この「遅れ」があることで、直線的すぎない、柔らかい動きになります。

7.マウス依存UIにしないための考え方

ここはかなり重要です。

スクロール+マウス連動UIを作るとき、ついマウス追従が主役になりがちです。
でも実際には、スマホで見たときに成立しないなら、そのUIは使いにくいままです。

設計としては次の考え方が安全です。

  • まずスクロールだけで完成させる
  • 次にPCだけマウス演出を足す
  • スマホで省略されても体験が壊れないようにする

この順番なら、演出を盛っても実用性を失いにくくなります。

8.よくある失敗

マウスがないと何も起きない

PCでしか成立しないUIになる典型です。
スマホで見たときに「静止画みたい」に見えてしまいます。

マウス補正が大きすぎる

動きが大きいと、読ませたいテキストより装飾の方が目立ってしまいます。
UIの主役を何にしたいのかがぼやけます。

スクロールとマウスの役割が曖昧

両方を同じ強さで動かすと、意図の分からない画面になりやすいです。
メインはスクロール、マウスは補助、と役割を分けると整理しやすくなります。

9.実験:スクロール主導+マウス補正のレイヤーUIを観察する

この実験では、スクロールによる移動をベースにしながら、PCではマウス位置で少しだけ奥行きを足しています。
スマホではマウス補正が無くても成立するように作っているので、画面幅や操作環境の違いも意識しながら見てみてください。

観察ポイント

  • スクロールだけでもレイアウトが成立しているか
  • PCではマウス補正が強すぎず、補助的に見えるか
  • 背景とカードで移動量が違うことで奥行きが出ているか
  • スマホで見たときに「演出が消えた」ではなく「シンプルに成立している」と感じるか

10.まとめ

スクロール+マウス連動UIは、少し加えるだけでも画面の印象を大きく変えられます。
ただし、マウス前提で作ってしまうとスマホで破綻しやすいため、まずスクロールだけで成立させることが大切です。

その上でPCだけマウス演出を足すと、実用性を保ちながら少しリッチなUIを作れます。
派手さよりも、壊れない設計を優先すると長く使える実装になります。

スクロール演出をさらに整理して見せたい場合は、Scroll Snap AnimationScroll Progress と組み合わせるのもおすすめです。


Scroll Animationシリーズ

コメント

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA