HTML / CSS / JS
スクロール
2026/03/30
2026/3/29
スクロールに合わせて画面中央のビジュアルが変化する演出は、ポートフォリオやブランドサイトで強い印象を与えるUIのひとつです。
このサンプルでは、CSSの3D表現とJavaScriptによるスクロール制御を組み合わせ、立方体(キューブ)が回転しながらシーンを切り替えるギャラリーを実装しています。
通常のスライダーとは異なり、「ページ全体が体験型UIになる」のが大きな特徴です。
視覚的インパクトを持たせたいLPや作品展示サイトに最適な構成になっています。
<div id="kumonosu-scene">
<div id="kumonosu-cube">
<div class="kumonosu-face" data-face="top" data-i="0"><span class="kumonosu-face-ph">TOP</span></div>
<div class="kumonosu-face" data-face="front" data-i="1"><span class="kumonosu-face-ph">FRONT</span></div>
<div class="kumonosu-face" data-face="right" data-i="2"><span class="kumonosu-face-ph">RIGHT</span></div>
<div class="kumonosu-face" data-face="back" data-i="3"><span class="kumonosu-face-ph">BACK</span></div>
<div class="kumonosu-face" data-face="left" data-i="4"><span class="kumonosu-face-ph">LEFT</span></div>
<div class="kumonosu-face" data-face="bottom" data-i="5"><span class="kumonosu-face-ph">BOTTOM</span></div>
</div>
</div>
<div id="kumonosu-hud">
<div id="kumonosu-hud-pct">000%</div>
<div class="kumonosu-progress-bar">
<div class="kumonosu-progress-fill" id="kumonosu-prog-fill"></div>
</div>
<div class="kumonosu-scene-label" id="kumonosu-scene-name">MOUNTAIN</div>
</div>
<div id="kumonosu-scene-strip"></div>
<div id="kumonosu-face-caption">
<div id="kumonosu-face-caption-num">01</div>
<div id="kumonosu-face-caption-name">MOUNTAIN</div>
</div>
<div id="kumonosu-scroll-container">
<section id="kumonosu-s0" class="kumonosu-section">
<div class="kumonosu-text-card">
<div class="kumonosu-tag">01 — Alpine Peak</div>
<h1 class="kumonosu-h1">SILENT<br>HEIGHTS</h1>
<p class="kumonosu-body-text">雲海を眼下に、静寂が支配する山の頂。そびえ立つ岩肌が太陽の光を浴びて輝きます。</p>
<div class="kumonosu-cta-row"><a class="kumonosu-cta" href="#kumonosu-s1">Explore</a></div>
</div>
</section>
<section id="kumonosu-s1" class="kumonosu-section">
<div class="kumonosu-text-card kumonosu-right">
<div class="kumonosu-tag">02 — Green Sanctuary</div>
<h2 class="kumonosu-h2">DEEP<br>WOODS</h2>
<p class="kumonosu-body-text">密に生い茂る木々の隙間から差し込む木漏れ日。生命の息吹を感じる豊かな森の光景。</p>
<div class="kumonosu-cta-row">
<a class="kumonosu-cta-back" href="#kumonosu-s0">Back</a>
<a class="kumonosu-cta" href="#kumonosu-s2">Turn</a>
</div>
</div>
</section>
<section id="kumonosu-s2" class="kumonosu-section">
<div class="kumonosu-text-card">
<div class="kumonosu-tag">03 — Earthy Path</div>
<h2 class="kumonosu-h2">OPEN<br>VALLEY</h2>
<p class="kumonosu-body-text">広大な大地へと続く一本の道。風が通り抜ける草原の中を、歩みを進めます。</p>
<div class="kumonosu-cta-row">
<a class="kumonosu-cta-back" href="#kumonosu-s1">Back</a>
<a class="kumonosu-cta" href="#kumonosu-s3">Turn</a>
</div>
</div>
</section>
<section id="kumonosu-s3" class="kumonosu-section">
<div class="kumonosu-text-card kumonosu-right">
<div class="kumonosu-tag">04 — Still Water</div>
<h2 class="kumonosu-h2">CRYSTAL<br>LAKE</h2>
<p class="kumonosu-body-text">鏡のように穏やかな湖面。周囲の山々を映し出すその姿は、時の流れを止めたかのよう。</p>
<div class="kumonosu-cta-row">
<a class="kumonosu-cta-back" href="#kumonosu-s2">Back</a>
<a class="kumonosu-cta" href="#kumonosu-s4">Turn</a>
</div>
</div>
</section>
<section id="kumonosu-s4" class="kumonosu-section">
<div class="kumonosu-text-card">
<div class="kumonosu-tag">05 — Cosmic Night</div>
<h2 class="kumonosu-h2">STARRY<br>GALAXY</h2>
<p class="kumonosu-body-text">夜空に広がる無数の星々と銀河。地球から遠く離れた宇宙の深淵に思いを馳せます。</p>
<div class="kumonosu-cta-row">
<a class="kumonosu-cta-back" href="#kumonosu-s3">Back</a>
<a class="kumonosu-cta" href="#kumonosu-s5">Turn</a>
</div>
</div>
</section>
<section id="kumonosu-s5" class="kumonosu-section">
<div class="kumonosu-text-card kumonosu-right">
<div class="kumonosu-tag">06 — Water's Edge</div>
<h2 class="kumonosu-h2">SUNSET<br>SHORE</h2>
<p class="kumonosu-body-text">夕暮れ時に染まる波打ち際。旅の終わりを告げる穏やかな波の音に耳を傾けて。</p>
<div class="kumonosu-cta-row">
<a class="kumonosu-cta-back" href="#kumonosu-s4">Back</a>
<a class="kumonosu-cta" href="#kumonosu-s0">Restart</a>
</div>
</div>
</section>
</div>
<div id="kumonosu-credit">
<a href="#">KUMONOSU GALLERY</a>
</div>
@import url("https://fonts.googleapis.com/css2?family=Bebas+Neue&family=DM+Mono:wght@300;400&display=swap");
@layer reset, tokens, base, layout, cube, ui, cards, reveal, responsive;
@layer reset {
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
}
@layer tokens {
:root {
color-scheme: dark;
--kumonosu-bg: #1c1814;
--kumonosu-fg: #ede8df;
--kumonosu-muted: #8a7b6e;
--kumonosu-accent: #d4a84b;
--kumonosu-font-display: "Bebas Neue", sans-serif;
--kumonosu-font-mono: "DM Mono", monospace;
--kumonosu-hairline: 0.0625rem;
--kumonosu-ui-inset: 2rem;
--kumonosu-card-bg: rgba(28, 24, 20, 0.82);
--kumonosu-card-border: rgba(212, 168, 75, 0.2);
--kumonosu-nav-x: calc(var(--kumonosu-ui-inset) + 0.125rem);
--kumonosu-reveal-offset: 0.625rem;
--kumonosu-reveal-duration: 0.5s;
--kumonosu-z-ui: 10;
}
}
@layer base {
body {
background: var(--kumonosu-bg);
color: var(--kumonosu-fg);
font-family: var(--kumonosu-font-mono);
overflow-x: hidden;
}
}
@layer layout {
#kumonosu-scene {
position: fixed;
inset: 0;
z-index: 0;
display: flex;
align-items: center;
justify-content: center;
perspective: 1100px;
pointer-events: none;
}
#kumonosu-scroll-container {
position: relative;
z-index: 1;
}
.kumonosu-section {
min-height: 100vh;
display: flex;
align-items: center;
padding: 6rem calc(5rem + var(--kumonosu-ui-inset)) 6rem 5rem;
}
}
@layer cube {
#kumonosu-cube {
--s: min(74vw, 74vh, 560px);
width: var(--s);
height: var(--s);
position: relative;
transform-style: preserve-3d;
transform: rotateX(90deg) rotateY(0deg);
will-change: transform;
}
.kumonosu-face {
position: absolute;
inset: 0;
overflow: hidden;
backface-visibility: hidden;
background: repeating-linear-gradient(0deg, rgba(255, 255, 255, 0.02) 0, rgba(255, 255, 255, 0.02) 1px, transparent 1px, transparent 48px),
repeating-linear-gradient(90deg, rgba(255, 255, 255, 0.02) 0, rgba(255, 255, 255, 0.02) 1px, transparent 1px, transparent 48px),
#14100d;
}
.kumonosu-face img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
.kumonosu-face-ph {
position: absolute;
bottom: 1.5rem;
left: 1.75rem;
font-family: var(--kumonosu-font-display);
font-size: clamp(2rem, 8vw, 5rem);
color: rgba(255, 255, 255, 0.06);
pointer-events: none;
}
.kumonosu-face[data-face="front"] {
transform: translateZ(calc(var(--s) / 2));
}
.kumonosu-face[data-face="back"] {
transform: rotateY(180deg) translateZ(calc(var(--s) / 2));
}
.kumonosu-face[data-face="right"] {
transform: rotateY(90deg) translateZ(calc(var(--s) / 2));
}
.kumonosu-face[data-face="left"] {
transform: rotateY(-90deg) translateZ(calc(var(--s) / 2));
}
.kumonosu-face[data-face="top"] {
transform: rotateX(-90deg) translateZ(calc(var(--s) / 2));
}
.kumonosu-face[data-face="bottom"] {
transform: rotateX(90deg) translateZ(calc(var(--s) / 2));
}
}
@layer ui {
#kumonosu-hud {
position: fixed;
top: var(--kumonosu-ui-inset);
right: var(--kumonosu-ui-inset);
z-index: var(--kumonosu-z-ui);
text-align: right;
font-size: 0.65rem;
letter-spacing: 0.15em;
color: var(--kumonosu-muted);
text-transform: uppercase;
}
.kumonosu-progress-bar {
width: 7.5rem;
height: var(--kumonosu-hairline);
background: var(--kumonosu-muted);
margin-top: 0.5rem;
margin-left: auto;
position: relative;
overflow: hidden;
}
.kumonosu-progress-fill {
position: absolute;
inset: 0;
width: 0%;
background: var(--kumonosu-accent);
transition: width 0.1s linear;
}
#kumonosu-scene-strip {
position: fixed;
left: var(--kumonosu-nav-x);
top: 50%;
translate: -50% -50%;
z-index: var(--kumonosu-z-ui);
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.kumonosu-scene-dot {
width: 0.25rem;
height: 0.25rem;
border-radius: 50%;
background: var(--kumonosu-muted);
transition: 0.3s;
cursor: pointer;
}
.kumonosu-scene-dot.kumonosu-active {
background: var(--kumonosu-accent);
scale: 1.8;
}
#kumonosu-face-caption {
position: fixed;
bottom: var(--kumonosu-ui-inset);
left: 50%;
translate: -50% 0;
z-index: var(--kumonosu-z-ui);
text-align: center;
pointer-events: none;
}
#kumonosu-face-caption-num {
font-size: 0.58rem;
color: var(--kumonosu-accent);
letter-spacing: 0.28em;
}
#kumonosu-face-caption-name {
font-family: var(--kumonosu-font-display);
font-size: clamp(1.8rem, 5vw, 3.5rem);
color: var(--kumonosu-muted);
opacity: 0.5;
}
#kumonosu-credit {
position: fixed;
right: var(--kumonosu-ui-inset);
top: 50%;
transform: translateY(-50%) rotate(-90deg);
transform-origin: right center;
font-size: 0.65rem;
letter-spacing: 0.15em;
text-transform: uppercase;
}
#kumonosu-credit a {
color: var(--kumonosu-muted);
text-decoration: none;
}
}
@layer cards {
.kumonosu-text-card {
max-width: 23.75rem;
padding: 2.25rem 2rem;
background: var(--kumonosu-card-bg);
border-left: var(--kumonosu-hairline) solid var(--kumonosu-card-border);
backdrop-filter: blur(6px);
transition: 0.3s;
}
.kumonosu-text-card.kumonosu-right {
margin-left: auto;
border-left: none;
border-right: var(--kumonosu-hairline) solid var(--kumonosu-card-border);
text-align: right;
}
.kumonosu-tag {
font-size: 0.6rem;
letter-spacing: 0.25em;
color: var(--kumonosu-accent);
margin-bottom: 1.1rem;
text-transform: uppercase;
}
.kumonosu-h1, .kumonosu-h2 {
font-family: var(--kumonosu-font-display);
line-height: 0.92;
font-weight: 400;
}
.kumonosu-h1 {
font-size: clamp(3rem, 8vw, 6.5rem);
}
.kumonosu-h2 {
font-size: clamp(2.2rem, 5vw, 4rem);
}
.kumonosu-body-text {
font-size: 0.78rem;
line-height: 1.8;
color: rgba(255, 255, 255, 0.55);
margin-top: 1.25rem;
}
.kumonosu-cta-row {
display: flex;
gap: 0.75rem;
margin-top: 1.75rem;
}
.kumonosu-text-card.kumonosu-right .kumonosu-cta-row {
justify-content: flex-end;
}
.kumonosu-cta, .kumonosu-cta-back {
padding: 0.6rem 1.25rem;
border: var(--kumonosu-hairline) solid var(--kumonosu-accent);
color: var(--kumonosu-accent);
font-size: 0.62rem;
text-transform: uppercase;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 0.6rem;
transition: 0.2s;
}
.kumonosu-cta:hover {
background: var(--kumonosu-accent);
color: var(--kumonosu-bg);
}
.kumonosu-cta-back {
border-color: rgba(128, 128, 128, 0.4);
color: var(--kumonosu-muted);
}
}
@layer reveal {
:is(.kumonosu-tag, .kumonosu-h1, .kumonosu-h2, .kumonosu-body-text, .kumonosu-cta, .kumonosu-cta-back) {
opacity: 0;
translate: 0 var(--kumonosu-reveal-offset);
transition: 0.5s ease;
}
:is(.kumonosu-tag, .kumonosu-h1, .kumonosu-h2, .kumonosu-body-text, .kumonosu-cta, .kumonosu-cta-back).kumonosu-visible {
opacity: 1;
translate: 0 0;
}
}
@layer responsive {
@media (max-width: 56.25em) {
#kumonosu-scene-strip {
display: none;
}
.kumonosu-section {
padding: 0 1.5rem 3.5rem;
align-items: flex-end;
}
.kumonosu-text-card {
max-width: 100%;
}
}
}
const IMAGE_SRCS = [
"https://images.unsplash.com/photo-1464822759023-fed622ff2c3b?auto=format&fit=crop&w=800&q=80",
"https://images.unsplash.com/photo-1441974231531-c6227db76b6e?auto=format&fit=crop&w=800&q=80",
"https://images.unsplash.com/photo-1447752875215-b2761acb3c5d?auto=format&fit=crop&w=800&q=80",
"https://images.unsplash.com/photo-1472214103451-9374bd1c798e?auto=format&fit=crop&w=800&q=80",
"https://images.unsplash.com/photo-1446776811953-b23d57bd21aa?auto=format&fit=crop&w=800&q=80",
"https://images.unsplash.com/photo-1470770841072-f978cf4d019e?auto=format&fit=crop&w=800&q=80"
];
const FACE_NAMES = ["MOUNTAIN", "FOREST", "VALLEY", "LAKE", "GALAXY", "SHORE"];
const N = IMAGE_SRCS.length;
const STOPS = [
{ rx: 90, ry: 0 }, { rx: 0, ry: 0 }, { rx: 0, ry: -90 },
{ rx: 0, ry: -180 }, { rx: 0, ry: -270 }, { rx: -90, ry: -360 }
];
const dom = {
cube: document.getElementById("kumonosu-cube"),
faces: [...document.querySelectorAll(".kumonosu-face")],
scrollEl: document.getElementById("kumonosu-scroll-container"),
strip: document.getElementById("kumonosu-scene-strip"),
hudPct: document.getElementById("kumonosu-hud-pct"),
progFill: document.getElementById("kumonosu-prog-fill"),
sceneName: document.getElementById("kumonosu-scene-name"),
captionNum: document.getElementById("kumonosu-face-caption-num"),
captionName: document.getElementById("kumonosu-face-caption-name")
};
IMAGE_SRCS.forEach((_, i) => {
const a = document.createElement("a");
a.href = `#kumonosu-s${i}`;
a.className = "kumonosu-scene-dot" + (i === 0 ? " kumonosu-active" : "");
dom.strip.appendChild(a);
});
const sceneDots = [...document.querySelectorAll(".kumonosu-scene-dot")];
async function setFaceImage(faceIdx, imgIdx) {
const face = dom.faces[faceIdx];
if (face.querySelector('img')) return;
const img = new Image();
img.src = IMAGE_SRCS[imgIdx];
face.appendChild(img);
}
const faceMap = [5, 1, 2, 3, 4, 0];
faceMap.forEach((faceIdx, i) => setFaceImage(faceIdx, i));
let lastIdx = -1;
const updateHUD = (s) => {
const p = Math.round(s * 100);
dom.hudPct.textContent = String(p).padStart(3, "0") + "%";
dom.progFill.style.width = `${p}%`;
const si = Math.max(0, Math.min(Math.floor(s * (N - 0.001)), N - 1));
if (si !== lastIdx) {
lastIdx = si;
dom.sceneName.textContent = FACE_NAMES[si];
dom.captionNum.textContent = String(si + 1).padStart(2, "0");
dom.captionName.textContent = FACE_NAMES[si];
sceneDots.forEach((d, i) => d.classList.toggle("kumonosu-active", i === si));
}
};
const easeIO = (t) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
const setCubeTransform = (s) => {
const t = s * (N - 1);
const i = Math.min(Math.floor(t), N - 2);
const f = easeIO(t - i);
const a = STOPS[i], b = STOPS[i + 1];
const rx = a.rx + (b.rx - a.rx) * f;
const ry = a.ry + (b.ry - a.ry) * f;
dom.cube.style.transform = `rotateX(${rx}deg) rotateY(${ry}deg)`;
};
let tgt = 0, smooth = 0;
window.addEventListener("scroll", () => {
const max = document.documentElement.scrollHeight - window.innerHeight;
tgt = max > 0 ? window.scrollY / max : 0;
}, { passive: true });
const io = new IntersectionObserver((entries) => {
entries.forEach(e => { if (e.isIntersecting) e.target.classList.add("kumonosu-visible"); });
}, { threshold: 0.1 });
document.querySelectorAll(".kumonosu-text-card > *").forEach(el => io.observe(el));
const frame = () => {
smooth += (tgt - smooth) * 0.1;
updateHUD(smooth);
setCubeTransform(smooth);
requestAnimationFrame(frame);
};
requestAnimationFrame(frame);
document.querySelectorAll('a[href^="#kumonosu-s"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
const target = document.querySelector(this.getAttribute('href'));
if (target) target.scrollIntoView({ behavior: 'smooth' });
});
});
Scroll量 → 進行率 → 回転角度 → 表示面切替
主な設定:
perspectivetransform-style: preserve-3dtranslateZ()rotateX() / rotateY()スクロールに応じて次の面へ滑らかに回転します。
一般的なスライダーではなく:
✅ スクロール駆動型スライダー
✅ ページ自体がタイムラインになる構造
特徴:
画面固定UIとして以下を表示:
スクロール状態を視覚的に把握できます。
テキスト要素は:
opacity + translate
による軽量演出です。
const IMAGE_SRCS = [
"image1.jpg",
"image2.jpg"
];
配列を変更するだけで差し替え可能。
const FACE_NAMES = ["MOUNTAIN", "FOREST"];
HUD・キャプションへ自動反映されます。
--s: min(74vw, 74vh, 560px);
数値を変更すると全体スケールが変わります。
const STOPS = [
{ rx: 90, ry: 0 }
];
ここを変更すると:
を自由に設計可能。
:root {
--kumonosu-accent:
--kumonosu-bg:
}
トークン設計されているためテーマ変更が容易です。
3D transform を常時使用するため:
position: fixed 依存キューブは固定レイヤーのため:
セクション数変更時は:
を一致させる必要があります。
3D描画はSafariで差が出る場合あり:
backface-visibility: hidden は必須スクロール主体UIのため:
を追加すると実運用向きになります。