画面縦スクロールでコンテンツを横スクロール : GSAPなしでハイブリッドスクロールを実装
画面縦スクロールでコンテンツを横スクロール : GSAPなしでハイブリッドスクロールを実装
画面を縦スクロールしていくと、途中からコンテンツが横にスクロールするWebページ、たまに見かけると思います。代表的な製品やその特徴を並列的に提示したり、手順などを順番に示したいケースで、印象的に表現することができます。
その実装にはJavaScriptライブラリのGSAPを利用する方法もありますが、多機能なだけに融通が効かず、思ったようにコントロールするのが難しい印象です。
今回は、素のJavaScriptとCSSでシンプルに実装してみます。
サンプルコード
HTML
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Hybrid Scroll Sample</title>
</head>
<body>
<h1>Hybrid Scroll Sample</h1>
<article>
<h2>article 1</h2>
</article>
<div class="horizontal_scroll">
<article class="sticky">
<h2>article 2</h2>
<div class="scroller">
<section>
<h3>section 1</h3>
</section>
<section>
<h3>section 2</h3>
</section>
<section>
<h3>section 3</h3>
</section>
</div>
</article>
</div>
<article>
<h2>article 3</h2>
</article>
<div class="horizontal_scroll">
<article class="sticky">
<h2>article 4</h2>
<div class="scroller">
<section>
<h3>section 1</h3>
</section>
<section>
<h3>section 2</h3>
</section>
<section>
<h3>section 3</h3>
</section>
<section>
<h3>section 4</h3>
</section>
<section>
<h3>section 5</h3>
</section>
<section>
<h3>section 6</h3>
</section>
</div>
</article>
</div>
</body>
</html>
CSS
@charset 'UTF-8';
body {
margin: 0;
overflow-x: clip;
}
article:not(.sticky) {
display: flex;
flex-direction: column;
justify-content: center;
height: 100vh;
background-color: whitesmoke;
}
article h2 {
text-align: center;
}
.horizontal_scroll {
--sticky-container-height: 100vh;
height: var(--sticky-container-height);
min-height: 100vh;
box-sizing: border-box;
}
.horizontal_scroll .sticky {
position: sticky;
top: 0;
display: flex;
flex-direction: column;
justify-content: center;
min-height: 100vh;
}
.horizontal_scroll .scroller {
display: flex;
overflow: auto;
}
.horizontal_scroll .scroller.nobar {
overflow: hidden;
}
.horizontal_scroll .scroller > * {
flex-basis: 66%;
flex-shrink: 0;
aspect-ratio: 16 / 9;
display: flex;
justify-content: center;
align-items: center;
background-color: whitesmoke;
}
.horizontal_scroll .scroller > *:not(:first-child) {
margin-left: 1%;
}
JavaScript
"use strict";
document.addEventListener("DOMContentLoaded", () => {
/* horizontal scroll */
const stickyContainers = document.querySelectorAll(".horizontal_scroll");
stickyContainers.forEach((stickyContainer) => {
// get elements
const stickyItem = stickyContainer.querySelector(".sticky");
const scroller = stickyContainer.querySelector(".scroller");
scroller.classList.add("nobar");
// set sticky height
const updateStickyHeight = () => {
const stickyHeight = scroller.scrollWidth - scroller.clientWidth + stickyItem.clientHeight;
stickyContainer.style.setProperty("--sticky-container-height", `${stickyHeight}px`);
};
updateStickyHeight();
new ResizeObserver(updateStickyHeight).observe(scroller);
new ResizeObserver(updateStickyHeight).observe(stickyItem);
// sync scroll
const syncScroll = () => {
const rect = stickyContainer.getBoundingClientRect();
if (rect.top <= 0 && rect.bottom >= window.innerHeight) {
scroller.scrollLeft = window.scrollY - stickyContainer.offsetTop;
} else if (rect.bottom < window.innerHeight) {
scroller.scrollLeft = scroller.scrollWidth - scroller.clientWidth;
} else {
scroller.scrollLeft = 0;
}
};
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
window.addEventListener("scroll", syncScroll, { passive: true });
syncScroll();
} else {
window.removeEventListener("scroll", syncScroll);
}
});
},
{ threshold: 0 }
);
observer.observe(stickyContainer);
// end of forEach
});
// end of DOMContentLoaded
});
サンプルDEMO
実際のサンプルページは[DEMOページ]ボタン押下で別タブ表示します。
コード解説
基本方針は次のとおりです。
- 画面の縦スクロール量分、コンテンツを横スクロールさせる
- 横スクロールしている間、縦スクロールは sticky で固定する
- sticky で固定するためにコンテナを設けて、横スクロール分を含めた高さを設定する
HTML
ここでは、body 要素に article 要素が複数並んでいます。
横スクロールさせたいコンテンツを含む article 要素に sticky クラスを付与します。これを sticky アイテムにしますので、sticky コンテナとして horizontal_scroll クラスを付与した要素でラップしています。
また、横スクロールさせたいコンテンツ群(ここでは section 要素)のコンテナ要素に scroller クラスを付与します。
以下で解説する CSS と JavaScript は、外部ファイルにして読み込むなり、インラインで記述するなりしてください。
複数設置したかったのでコードが長くなってしまいましたが、構造はとても単純です。
CSS
23-30行目の sticky クラス要素への指定において、sticky 配置でコンテナ最上部に固定、最小の高さを 100vh に指定しています。
そのコンテナ要素である horizontal_scroll クラス要素は、17-22行目で指定しています。コンテンツの横スクロール分を含めた高さを設定したいのですが、予見できないので CSS 変数 "--sticky-container-height" を設定しています。のちほど JavaScript で動的に設定しますが、一応、初期値として 100vh が指定してあります。
横スクロールのコンテナとなる scroller クラス要素は、31-34行目で指定しています。flex でアイテムを横に並べて、overflow を auto にして横スクロールバーが表示されるようにしています(flex アイテムは40行目で、縮まずに溢れるようにしてあります)。
続けて35-37行目で、同要素に JavaScript で nobar クラスが付加されたときに overflow が hidden に変わって隠れるようにしています。これは、JavaScript が無効の状態でも、横スクロールでコンテンツを表示できるようにする配慮です。ただし、ファーストビューに含まれる場合は、表示がチラつく可能性があるので、初めから hidden にしておいた方がよいかもしれません。
最後に3-6行目の body への指定で、overflow-x を clip にしています。この指定は、今回のサンプルでは必要ないのですが、overflow を hidden にしている場合はその子孫要素で sticky 配置が効かないため、その代替手段を示すために記述しています。
例えば、"margin: 0 calc(50% - 50vw);" との併用で "body { overflow-x: hidden; }" とする手法はよくみられますが、clip に置き換えてください。同じように横スクロールを防ぐことができ、かつ、sticky が使えます。
以上が要点で、ほかの記述は体裁を整えているだけです。
JavaScript
14-20行目で、先の CSS 変数 "--sticky-container-height" を設定しています。sticky コンテナの高さになるので、sticky アイテムの高さ(clientHeight)に、コンテンツの横スクロール幅を足した値を与えています。
scrollWidth は overflow 分も含めたコンテンツ幅を取得するため、そこから clientWidth を引いて溢れ分のみの横スクロール幅を計算しています。
この関数 "updateStickyHeight" を、sticky アイテム(.sticky)や横スクロールコンテナ(.scroller)がリサイズした時と、初期時に実行しています。リサイズオブザーバーを使用することで、再計算が必要な時のみ効率よく実行されます。
23-32行目が、画面の縦スクロール量分、コンテンツを横スクロールさせる関数です。
sticky コンテナ(.horizontal_scroll)の上下が画面を覆っている時、画面の縦スクロール位置とsticky コンテナ上辺との差分を、横スクロールコンテナ(.scroller)のスクロール位置に指定しています。
なお、sticky コンテナが画面より上に通り過ぎた時には最大限スクロールした状態に、下に通り過ぎた時にはスクロールしていない状態に指定されます。この処理により、sticky コンテナが画面より上に通り過ぎた状態でページをリロードした場合でも、意図どおり横スクロールされた状態になります。
この関数 "syncScroll" を、33-46行目の交差オブザーバーで実行しています。sticky コンテナ(.horizontal_scroll)がビューポートと交差している時のみ、scroll イベントの発生を待機します。
画面をスクロールする度に処理が発生するため、必要な時だけ実行するようにして負荷の低減を図っています。
少し冗長な部分もありますが、やっていることはシンプルではないでしょうか。
おわりに
通常の縦スクロールと横スクロールを織り交ぜたこうした効果を、ハイブリッドスクロールと呼んだりするようです。印象的なユーザー体験を、比較的シンプルな実装で実現できました。
ですが、デザイン設計には注意も必要です。例えば、横スクロールするコンテンツがあまりに多いと、ユーザーはそれ以降のコンテンツになかなかたどりつけません。
また、ブラウザでの縦スクロールが効いていない(ように見える)ことは、ブラウザUIへの信頼性を毀損することにもなりかねません。今回のようなケースはセーフと考えますが、基本となるブラウザのユーザーインターフェースに関与するような効果の導入は、慎重な姿勢で検討すべきと心得ておきたいものです。
ソースコードの改善点などありましたら、ご指摘いただければうれしいです。Twitter(現 𝕏)は @uncl010 まで(あまり公私分けてないですが)。
それではまた、Happy coding !