スクロール

CSSやGSAPで作る、カードが散らばるパララックスヒーロー

投稿日2026/09/07

更新日2026/9/6

ページを開くとカードが上空から次々と降ってきて横一列に並び、マウスを動かせば奥行きに応じてカードが揺れ、スクロールすると放射状に散らばっていく――そんな動きの密度が高いヒーローセクションを、GSAPとCSSで実装したデモです。

各カードにはdata属性で傾き(rot)と奥行き(depth)が設定されており、イントロ・フロート・パララックス・ホバー3Dチルト・スクロール散開と、5種類のアニメーションが自然に重なり合います。

背景にはSVGベースのグレインテクスチャをオーバーレイし、ざらっとした質感がデザインに奥行きを加えています。

ポートフォリオやクリエイティブ系サイトのファーストビューに最適です。

Code コード

<div class="kumonosu-grain"></div>
<section class="kumonosu-hero">
	<div class="kumonosu-cards-row" id="kumonosu-cards-container">
		<div class="kumonosu-card kumonosu-card-1" data-rot="-9" data-depth="14">
			<img src="https://kumonosu.net/wp-content/uploads/2026/01/260114c.jpeg.webp" alt="" />
		</div>
		<div class="kumonosu-card kumonosu-card-2" data-rot="-5" data-depth="10">
			<img src="https://kumonosu.net/wp-content/uploads/2026/01/176a.jpg.webp" alt="" />
		</div>
		<div class="kumonosu-card kumonosu-card-3" data-rot="-2" data-depth="8">
			<img src="https://kumonosu.net/wp-content/uploads/2026/01/176b.jpg.webp" alt="" />
		</div>
		<div class="kumonosu-card kumonosu-card-4" data-rot="3" data-depth="12">
			<img src="https://kumonosu.net/wp-content/uploads/2026/01/176c.jpg.webp" alt="" />
		</div>
		<div class="kumonosu-card kumonosu-card-5" data-rot="0" data-depth="6">
			<img src="https://kumonosu.net/wp-content/uploads/2026/01/176f.jpg.webp" alt="" />
		</div>
		<div class="kumonosu-card kumonosu-card-6" data-rot="4" data-depth="11">
			<img src="https://kumonosu.net/wp-content/uploads/2026/01/176e.jpg.webp" alt="" />
		</div>
		<div class="kumonosu-card kumonosu-card-7" data-rot="7" data-depth="9">
			<img src="https://kumonosu.net/wp-content/uploads/2026/01/176g.jpg.webp" alt="" />
		</div>
		<div class="kumonosu-card kumonosu-card-8" data-rot="-4" data-depth="13">
			<img src="https://kumonosu.net/wp-content/uploads/2026/01/260114c.jpeg.webp" alt="" />
		</div>
	</div>
</section>
:root {
	--kumonosu-bg: #f5efe2;
	--kumonosu-ink: #141416;
}
* {
	margin: 0;
	padding: 0;
	box-sizing: border-box;
}
html,
body {
	background: var(--kumonosu-bg);
	color: var(--kumonosu-ink);
	font-family: "DM Sans", system-ui, sans-serif;
	-webkit-font-smoothing: antialiased;
}
body {
	background: radial-gradient(120% 70% at 50% 0%,
			#f8f3e7 0%,
			#f2ead6 60%,
			#e9dec3 100%);
	min-height: 200vh;
}
.kumonosu-grain {
	position: fixed;
	inset: 0;
	pointer-events: none;
	z-index: 100;
	opacity: 0.06;
	mix-blend-mode: multiply;
	background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHdpZHRoPScyMjAnIGhlaWdodD0nMjIwJz48ZmlsdGVyIGlkPSduJz48ZmVUdXJidWxlbmNlIHR5cGU9J2ZyYWN0YWxOb2lzZScgYmFzZUZyZXF1ZW5jeT0nMC44NScgbnVtT2N0YXZlcz0nMicgc3RpdGNoVGlsZXM9J3N0aXRjaCcvPjxmZUNvbG9yTWF0cml4IHZhbHVlcz0nMCAwIDAgMCAwLjEgIDAgMCAwIDAgMC4xICAwIDAgMCAwIDAuMSAgMCAwIDAgMC42IDAnLz48L2ZpbHRlcj48cmVjdCB3aWR0aD0nMTAwJScgaGVpZ2h0PScxMDAlJyBmaWx0ZXI9J3VybCgjbiknLz48L3N2Zz4=");
}
.kumonosu-hero {
	position: fixed;
	top: 0;
	left: 0;
	width: 100%;
	height: 100vh;
	display: flex;
	align-items: center;
	justify-content: center;
	z-index: 5;
	overflow: hidden;
	pointer-events: none;
}
.kumonosu-cards-row {
	position: relative;
	width: 100%;
	height: 320px;
	z-index: 3;
	pointer-events: none;
}
.kumonosu-cards-row>* {
	pointer-events: auto;
}
.kumonosu-card {
	position: absolute;
	border-radius: 18px;
	overflow: hidden;
	background: #d1cbc0;
	box-shadow: 0 30px 60px -12px rgba(0, 0, 0, 0.3),
		0 18px 36px -18px rgba(0, 0, 0, 0.4),
		inset 0 1px 0 rgba(255, 255, 255, 0.25);
	cursor: pointer;
	will-change: transform;
	transition: box-shadow 0.5s cubic-bezier(0.6, 0, 0.2, 1);
}
.kumonosu-card img {
	width: 100%;
	height: 100%;
	object-fit: cover;
	object-position: center top;
	display: block;
}
.kumonosu-card:hover {
	box-shadow: 0 50px 80px -20px rgba(0, 0, 0, 0.4),
		0 30px 50px -30px rgba(0, 0, 0, 0.5),
		inset 0 1px 0 rgba(255, 255, 255, 0.3);
}
.kumonosu-card-1 {
	width: 130px;
	height: 180px;
	left: 4%;
	top: 30px;
}
.kumonosu-card-2 {
	width: 160px;
	height: 220px;
	left: 12%;
	top: 50px;
}
.kumonosu-card-3 {
	width: 200px;
	height: 270px;
	left: 22%;
	top: 20px;
}
.kumonosu-card-4 {
	width: 150px;
	height: 200px;
	left: 36%;
	top: 70px;
}
.kumonosu-card-5 {
	width: 230px;
	height: 310px;
	left: 44%;
	top: 0px;
}
.kumonosu-card-6 {
	width: 160px;
	height: 215px;
	left: 59%;
	top: 55px;
}
.kumonosu-card-7 {
	width: 175px;
	height: 240px;
	left: 70%;
	top: 30px;
}
.kumonosu-card-8 {
	width: 130px;
	height: 175px;
	left: 84%;
	top: 50px;
}
@media (max-width: 750px) {
	.kumonosu-cards-row {
		height: 220px;
	}
	.kumonosu-card-1 {
		width: 80px;
		height: 110px;
		left: 0%;
		top: 20px;
	}
	.kumonosu-card-2 {
		width: 90px;
		height: 130px;
		left: 10%;
		top: 35px;
	}
	.kumonosu-card-3 {
		width: 110px;
		height: 150px;
		left: 22%;
		top: 15px;
	}
	.kumonosu-card-4 {
		width: 90px;
		height: 120px;
		left: 36%;
		top: 50px;
	}
	.kumonosu-card-5 {
		width: 130px;
		height: 180px;
		left: 46%;
		top: 0;
	}
	.kumonosu-card-6 {
		width: 95px;
		height: 130px;
		left: 62%;
		top: 40px;
	}
	.kumonosu-card-7 {
		width: 100px;
		height: 140px;
		left: 74%;
		top: 20px;
	}
	.kumonosu-card-8 {
		width: 80px;
		height: 110px;
		left: 87%;
		top: 35px;
	}
}
gsap.registerPlugin(ScrollTrigger);
// INITIAL STATES
document.querySelectorAll(".kumonosu-card").forEach((card) => {
	const rot = parseFloat(card.dataset.rot) || 0;
	card.dataset.restRot = rot;
	gsap.set(card, {
		y: -800,
		rotation: rot + 25,
		opacity: 0,
		scale: 0.7
	});
});
// INTRO TIMELINE
gsap.to(".kumonosu-card", {
	y: 0,
	opacity: 1,
	scale: 1,
	rotation: (i, el) => parseFloat(el.dataset.restRot) || 0,
	duration: 1.2,
	stagger: {
		each: 0.1,
		from: "center"
	},
	ease: "back.out(1.4)",
	delay: 0.2
});
// FLOAT
document.querySelectorAll(".kumonosu-card").forEach((card, i) => {
	const rot = parseFloat(card.dataset.restRot) || 0;
	gsap.to(card, {
		y: `+=${10 + (i % 3) * 5}`,
		rotation: rot + (i % 2 === 0 ? 1.5 : -1.5),
		duration: 3 + (i % 4) * 0.5,
		delay: 1.5 + i * 0.1,
		ease: "sine.inOut",
		yoyo: true,
		repeat: -1
	});
});
// PARALLAX
let kumonosu_mx = 0,
	kumonosu_my = 0,
	kumonosu_tx = 0,
	kumonosu_ty = 0;
window.addEventListener("mousemove", (e) => {
	kumonosu_mx = (e.clientX / window.innerWidth - 0.5) * 2;
	kumonosu_my = (e.clientY / window.innerHeight - 0.5) * 2;
});

function kumonosu_parallax() {
	kumonosu_tx += (kumonosu_mx - kumonosu_tx) * 0.05;
	kumonosu_ty += (kumonosu_my - kumonosu_ty) * 0.05;
	document.querySelectorAll(".kumonosu-card").forEach((card) => {
		const d = parseFloat(card.dataset.depth) || 8;
		card.style.translate = `${kumonosu_tx * d}px ${kumonosu_ty * d * 0.5}px`;
	});
	requestAnimationFrame(kumonosu_parallax);
}
kumonosu_parallax();
// CARD HOVER 3D
document.querySelectorAll(".kumonosu-card").forEach((card) => {
	card.addEventListener("mousemove", (e) => {
		const r = card.getBoundingClientRect();
		const px = (e.clientX - r.left) / r.width - 0.5;
		const py = (e.clientY - r.top) / r.height - 0.5;
		gsap.to(card, {
			rotateX: -py * 20,
			rotateY: px * 20,
			scale: 1.1,
			zIndex: 20,
			duration: 0.4,
			ease: "power2.out",
			transformPerspective: 800,
			overwrite: "auto"
		});
	});
	card.addEventListener("mouseleave", () => {
		gsap.to(card, {
			rotateX: 0,
			rotateY: 0,
			scale: 1,
			zIndex: card.style.zIndex || "",
			duration: 0.8,
			ease: "elastic.out(1, 0.6)",
			overwrite: "auto"
		});
	});
});
// SCROLL TRIGGER
ScrollTrigger.create({
	trigger: "body",
	start: "top top",
	end: "bottom bottom",
	scrub: 1,
	onUpdate: (self) => {
		const p = self.progress;
		const moves = [{
			x: -350,
			y: -150,
			rot: -40
		}, {
			x: -280,
			y: -50,
			rot: -25
		}, {
			x: -180,
			y: 50,
			rot: -15
		}, {
			x: -80,
			y: 120,
			rot: -5
		}, {
			x: 80,
			y: 120,
			rot: 5
		}, {
			x: 180,
			y: 50,
			rot: 15
		}, {
			x: 280,
			y: -50,
			rot: 25
		}, {
			x: 350,
			y: -150,
			rot: 40
		}];
		document.querySelectorAll(".kumonosu-card").forEach((card, i) => {
			const m = moves[i];
			const rest = parseFloat(card.dataset.restRot) || 0;
			gsap.set(card, {
				x: m.x * p,
				y: m.y * p,
				rotation: rest + m.rot * p
			});
		});
	}
});
https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js
https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/ScrollTrigger.min.js

Explanation 詳しい説明

仕様

このデモは、GSAPのアニメーションとCSSの3D transformを組み合わせて、複数のインタラクションが同時に機能するヒーローセクションを実装しています。

主な機能は以下のとおりです。

  • イントロアニメーション: ページ読み込み時に、8枚のカードが画面上部(y: -800)から回転+縮小した状態で降ってきて、元の位置・角度・スケールに着地します。stagger: { from: "center" }で中央のカードから外側に広がる順序で出現します。
  • フロートアニメーション: 着地後、各カードが個別のリズム(3〜4.5秒周期)でゆるやかに上下に揺れ続けます。yoyo: true, repeat: -1による無限ループです。
  • マウスパララックス: マウス位置に応じて各カードが横・縦に微妙にずれます。data-depth属性の値が大きいカードほど大きく動き、奥行き感のある視差効果を生みます。requestAnimationFrameで補間しながら追従します。
  • ホバー3Dチルト: カード上にマウスを載せると、カーソル位置に応じてrotateXrotateYが変化し、3Dチルト効果が発生します。離すとelastic.outイージングで元に戻ります。
  • スクロール散開: ScrollTriggerでスクロール進行率に応じて、各カードが放射状に移動+回転します。左側のカードは左上に、右側は右上に散らばっていきます。
  • グレインテクスチャ: SVGのfeTurbulenceフィルターをBase64化してCSSのbackground-imageとして適用し、mix-blend-mode: multiplyで全体にざらついた質感をオーバーレイしています。
  • レスポンシブ対応: 750px以下ではカードサイズとカード行の高さが縮小されます。

カスタマイズ

  • 画像の変更: 各.kumonosu-card内の<img>srcを差し替えるだけで画像を変更できます。
  • カードの枚数: HTMLにカードを追加し、CSSで位置クラス(.kumonosu-card-N)を定義、JSのmoves配列にスクロール散開の座標を追加すれば枚数を変えられます。
  • カードの初期角度: data-rot属性の値を変更すると、各カードの静止時の傾きが変わります。
  • パララックスの強さ: data-depth属性の値を変更すると、マウスパララックスの移動量が変わります。値が大きいほど大きく動きます。
  • イントロのイージング: back.out(1.4)の値を変更すると、着地時のバウンス感を調整できます。
  • フロートの振幅: y: +=10 + (i % 3) * 5の値を変更すると、浮遊の上下幅が変わります。
  • スクロール散開の距離: moves配列内のx・y・rot値を変更すると、スクロール時の散らばり方を調整できます。
  • 背景色: CSS変数--kumonosu-bgとbodyのbackgroundグラデーションを変更すると、全体の雰囲気が変わります。
  • グレインの濃さ: .kumonosu-grainopacity(デフォルト0.06)を変更すると、ざらつきの強さが変わります。

注意点

  • GSAPの読み込み: CDN(cdnjs.cloudflare.com)からGSAP 3.12.5とScrollTriggerを読み込んでいます。本番環境ではバージョン固定やローカルホスティングを推奨します。
  • アニメーションの重なり: 5種のアニメーション(イントロ・フロート・パララックス・ホバー・スクロール)が同時に動作するため、GSAPのoverwrite: "auto"でホバー時の競合を制御しています。独自にアニメーションを追加する場合は競合に注意してください。
  • スクロール領域: bodymin-height: 200vhが設定されており、ヒーロー自体はposition: fixedで固定表示されます。後続コンテンツを追加する場合はmin-heightの調整が必要です。
  • モバイルでのパララックス: マウスパララックスはmousemoveイベントに依存しているため、タッチのみの端末では機能しません。フロートとスクロール散開は正常に動作します。
  • パフォーマンス: requestAnimationFrameでパララックスを毎フレーム更新し、さらにGSAPのフロートとScrollTriggerが並行するため、低スペック端末では処理が重くなる場合があります。will-change: transformでGPU合成を促進しています。
  • カードの重なり順: ホバー時にzIndex: 20を設定して最前面に表示しますが、mouseleave後の戻り処理では元のzIndexが完全に復元されない場合があります。