スクロールに合わせて要素が少し動くだけでも、UIの印象はかなり変わります。
さらにPCではマウス位置に合わせてわずかに動きを足すと、画面に奥行きや気持ちよさが出ます。
ただし、ここで注意したいのがマウス前提の設計にしないことです。
スマホではマウスイベントが使えないため、mousemove がないと成立しないUIにしてしまうと、PCでは動くのにスマホでは何も起きない状態になります。これでは演出ではなく、ただの不親切なUIになってしまいます。
この記事では、スクロールだけで成立するUIをベースにして、PCだけマウス演出を追加する方法を紹介します。
「スマホでも破綻しない」「でもPCだとちょっと気持ちいい」そんなバランスの良い実装です。
この記事で扱う内容
- スクロールを主役にしたUIアニメーション
- PCではマウスで少しリッチに見せる
- スマホではスクロールだけで成立させる
transformを使って軽く動かす- マウス依存UIにしない考え方
スクロール量に応じて要素を動かす基本形は、先に Scroll Trigger Animation や Parallax Scroll Animation を見ておくと入りやすいです。
1.スクロール+マウス連動UIとは?
スクロール+マウス連動UIは、次の2つを組み合わせた表現です。
- スクロール量に応じて要素を動かす
- マウス位置に応じて要素を少しだけ補正する
たとえばカードや背景の円、装飾パーツなどを少しずつズラすと、平面的なレイアウトでも立体感のある見え方になります。
ただし重要なのは、主役はスクロールであることです。
マウスはあくまで補助演出にしておくと、スマホでも自然に成立します。
2.まずは設計の考え方
このUIは、次の順番で考えると破綻しにくいです。
①まずスクロールだけで成立させる
最初に、スクロール量だけでアニメーションが成立するように作ります。
この時点でスマホでもPCでも見せたい体験が完成している状態が理想です。
②その上でPCだけマウス演出を足す
次に、ポインタが細かく使える端末だけでマウス演出を追加します。
これなら、PCでは少しリッチに、スマホではシンプルに見せられます。
③動かしすぎない
マウス追従を大きくしすぎると、UIより演出が前に出て読みにくくなります。
数px〜十数px程度の小さな補正に留めるのがコツです。
スクロール中の見せ方を固定レイアウト込みで考えたい場合は、Sticky Scroll Animation や Scroll 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 Animation や Scroll Progress と組み合わせるのもおすすめです。
Scroll Animationシリーズ

コメントを残す