ページをスクロールしてサイドバーが下端まで来たら固定して追従。
サイトのレイアウトが2カラムのときに、ページをスクロールしてサイドバーが下端まで来たら固定して追従させる方法。
ページのスクロール位置に応じてJavaScriptでサイドバーに任意のクラス名を動的に付与・削除して、追従の挙動自体はCSSのpositionプロパティーで制御する。
HTMLのマークアップとCSSの設定
ページのスクロールで変動するY座標で制御するためには、隣合う2カラムの高さが同じである必要があるので、CSS Flexible Box LayoutかCSS Grid Layoutを使って2カラムにレイアウトする。
(「float」プロパティーなどで2カラムレイアウトしている場合は、JavaScriptでそれぞれの高さを取得・変更して揃える作業が必要。)
HTMLのマークアップ
2カラムレイアウト用のマークアップ。
- カラムを構成するmain要素とaside要素は同じ高さになるように設計する。
- コンテンツの高さ(合計の高さの実測値)を取得する必要があるのでdiv要素(「#main-inner」と「#sidebar-inner」)でラップをする。
※ 実際に追従の挙動をするのは「#sidebar-inner」のdiv要素。(スクロールの位置によって、「#sidebar-inner」が「#sidebar」の上端か下端に張り付く。)
<div id="container">
<main id="main">
<div id="main-inner">
<div>メインのコンテンツ</div>
<div>メインのコンテンツ</div>
...
</div>
</main>
<aside id="sidebar">
<div id="sidebar-inner">
<div>サイドバーのコンテンツ</div>
<div>サイドバーのコンテンツ</div>
...
</div>
</aside>
</div>
CSSの設定
flexを使った2カラムレイアウト。
- 「#sidebar-inner」のdiv要素にaside要素と同じwidthの値を指定しておく必要がある。(サイドバーが「position: fixed;」になったときのため。)
- aside要素(「#sidebar-inner」の親要素)に「position: relative;」を指定しておく必要がある。(サイドバーが「position: absolute;」になったときのため。)
※ スクロール位置に応じてJavaScriptで動的に付与するクラス名のCSS設定も記述しておく。
#container {
display: -ms-flexbox;/* IE11とIE10対応 */
display: flex;
}
main {
-ms-flex-positive: 1;/* IE10対応 */
flex-grow: 1;
}
aside {
flex-shrink: 0;
width: 300px;
position: relative;
}
#sidebar-inner {
width: 300px;
}
/* サイドバー追従制御用のCSS */
.sidebar-fixedtop {
position: fixed;
top: 0;
}
.sidebar-fixedbottom {
position: fixed;
bottom: 0;
}
.sidebar-absobottom {
position: absolute;
bottom: 0;
}
positionプロパティーが「fixed」か「absolute」のときに「left」も「right」も指定しなかった場合は、「static」のときの位置のX座標が割り当てられる。
JavaScriptで制御
スクロール位置に応じて、「#sidebar-inner」のdiv要素に任意のクラス名を動的に付与・削除してサイドバーの追従を制御する。
- スクロールトップのY座標を検知。
- 所定のY座標に来たらサイドバーに任意のクラス名を付与・削除。
- クラス名に応じてCSSのpositonプロパティーのスタイルが適用される。
分岐の条件
表示ウィンドウの高さ、コンテンツの高さによって挙動が異なるので、下記の3通りの条件分岐で対応。
- 「メインカラムコンテンツ」の高さが「表示ウィンドウ」または「サイドバーコンテンツ」の高さ以下の場合。
- サイドバーはスクロールに対し通常の動作を行う。(常に「position: static;」)
- 「メインカラムコンテンツ」の高さが「表示ウィンドウ」の高さより大きい場合。
2-1. 「サイドバーコンテンツ」の高さが「表示ウィンドウ」の高さ以下の場合。- スクロールが「サイドバー」上端に来るまでは通常の動作。(「position: static;」)
- スクロールが「サイドバー」上端まで来たら「サイドバー」上端を「ウィンドウトップ」に固定して追従。(「position: fixed;」で「top: 0;」)
※ ヘッダーが上部固定のデザインの場合は、「top」の値をヘッダーの高さにしてずらす。 - スクロールが「メインカラム」下端まで来たら固定を解除して「サイドバー」下端を「カラム」下端に固定。(「position: absolute;」で「bottom: 0;」)
2-2. 「サイドバーコンテンツ」の高さが「表示ウィンドウ」の高さより大きい場合。
- スクロールが「サイドバー」下端に来るまでは通常の動作。(「position: static;」)
- スクロールが「サイドバー」下端まで来たら「サイドバー」下端を「ウィンドウボトム」に固定して追従。(「position: fixed;」で「bottom: 0;」)
- スクロールが「メインカラム」下端まで来たら固定を解除して「サイドバー」下端を「カラム」下端に固定。(「position: absolute;」で「bottom: 0;」)
ネイティブJavaScript(Vanilla JavaScript)で実装する場合
1. 制御に必要な要素をDOMから取得。
//DOMから指定要素を取得
const sidebar = document.getElementById('sidebar'),//サイドバー
sidebarInner = document.getElementById('sidebar-inner'),//サイドバーコンテンツ
mainInner = document.getElementById('main-inner');//メインコンテンツ
document.getElementById(‘ID’) : 指定されたIDに一致する要素を表すElementオブジェクトを返す。無ければ「null」を返す。
2. 「#sidebar-inner」のdiv要素に任意のクラス名を付与・削除する関数を作成。
//付与するクラス名
const sidebarFixedTop = 'sidebar-fixedtop',
sidebarFixedBottom = 'sidebar-fixedbottom',
sidebarAbsoBottom = 'sidebar-absobottom';
//クラス付与・削除の関数
const ag2sidebarClass = {
addFixedTop: function(){
if(!sidebarInner.classList.contains(sidebarFixedTop)){
sidebarInner.classList.add(sidebarFixedTop);
if(sidebarInner.classList.contains(sidebarFixedBottom)) sidebarInner.classList.remove(sidebarFixedBottom);
if(sidebarInner.classList.contains(sidebarAbsoBottom)) sidebarInner.classList.remove(sidebarAbsoBottom);
}
},
addFixedBottom: function(){
if(!sidebarInner.classList.contains(sidebarFixedBottom)){
sidebarInner.classList.add(sidebarFixedBottom);
if(sidebarInner.classList.contains(sidebarFixedTop)) sidebarInner.classList.remove(sidebarFixedTop);
if(sidebarInner.classList.contains(sidebarAbsoBottom)) sidebarInner.classList.remove(sidebarAbsoBottom);
}
},
addAbsoBottom: function(){
if(!sidebarInner.classList.contains(sidebarAbsoBottom)){
sidebarInner.classList.add(sidebarAbsoBottom);
if(sidebarInner.classList.contains(sidebarFixedTop)) sidebarInner.classList.remove(sidebarFixedTop);
if(sidebarInner.classList.contains(sidebarFixedBottom)) sidebarInner.classList.remove(sidebarFixedBottom);
}
},
removeAll: function(){
if(sidebarInner.classList.contains(sidebarFixedTop)) sidebarInner.classList.remove(sidebarFixedTop);
if(sidebarInner.classList.contains(sidebarFixedBottom)) sidebarInner.classList.remove(sidebarFixedBottom);
if(sidebarInner.classList.contains(sidebarAbsoBottom)) sidebarInner.classList.remove(sidebarAbsoBottom);
}
};
要素.classList : 要素のclass属性を返す読み取り専用のプロパティー。DOMTokenListのコレクション。
要素.classList.contains(‘クラス名’) : 要素のclassList(DOMTokenList)に指定したクラス名が含まれていれば「true」、無ければ「false」を返す。
要素.classList.add(‘クラス名’) : 指定されたクラス名を要素のclassListに追加する。
要素.classList.remove(‘クラス名’) : 指定されたクラス名を要素のclassListから削除する。
3. 各要素の高さとページの現在のY座標を取得し、各要素の高さによるレイアウト構成とY座標の位置に応じて、上記のクラス名付与の関数を実行する関数を作成。
※ 各要素の座標と高さはロード時とリサイズ時のみ取得すれば良いので、膨大に発火するスクロールイベントでは処理しないようにする。
//各数値を保持する変数
let currentY,
sidebarRect,
sidebarInnerRect,
mainInnerRect,
winH,
sidebarH,
sidebarInnerH,
mainInnerH,
sidebarTop,
sidebarBottom;
//Y座標によってサイドバーのクラス名を書き換える関数
const ag2sidebarFix = function(e){
//windowの現在のY座標(スクロール量)
currentY = window.pageYOffset;
//ロードとリサイズのときは各要素の情報を取得
if(e.type !== 'scroll'){
//各要素の情報オブジェクトを取得
sidebarRect = sidebar.getBoundingClientRect(),
sidebarInnerRect = sidebarInner.getBoundingClientRect(),
mainInnerRect = mainInner.getBoundingClientRect();
//各要素の高さを取得
winH = window.innerHeight,
sidebarH = sidebarRect.height,
sidebarInnerH = sidebarInnerRect.height,
mainInnerH = mainInnerRect.height;
//document左上を基準点としたsidebarの上下端の座標(サイト内での絶対座標)
//(ブラウザ基準の相対座標に現在のスクロール量を加算して絶対座標に変換)
sidebarTop = sidebarRect.top + currentY,
sidebarBottom = sidebarRect.bottom + currentY;
}
//メインコンテンツの高さがwindow、またはサイドバー以下の場合
if(mainInnerH <= winH || mainInnerH <= sidebarInnerH){
ag2sidebarClass.removeAll();
//サイドバーの高さがwindow以下の場合
}else if(sidebarInnerH <= winH){
//2カラムの最下端が見えている場合
if(currentY + sidebarInnerH >= sidebarBottom){
ag2sidebarClass.addAbsoBottom();
//スクロールが2カラムの上端を超えた場合
}else if(currentY > sidebarTop){
ag2sidebarClass.addFixedTop();
}else{
ag2sidebarClass.removeAll();
}
//サイドバーがwindowより長い場合
}else{
//2カラムの最下端が見えている場合
if(currentY + winH >= sidebarBottom){
ag2sidebarClass.addAbsoBottom();
//サイドバーの下端が見えている場合
}else if(currentY + winH > sidebarTop + sidebarInnerH){
ag2sidebarClass.addFixedBottom();
}else{
ag2sidebarClass.removeAll();
}
}
};
window.pageYOffset : 現在垂直方向にスクロールしているピクセル数を返す。scrollYプロパティーのエイリアス。(ブラウザ互換性を考慮してpageYOffsetを使う。)
event.type : Eventインターフェイスの読取専用プロパティーで、イベントの種別を表す文字列を返す。
要素.getBoundingClientRect() : 指定した要素の寸法と、そのビューポートの左上からの相対位置を返す。読み取り専用の「left, top, right, bottom, x, y, width, height」のプロパティーを持つ。
window.innerHeight : Windowインターフェイスの読み取り専用プロパティーで、ウィンドウの内部の高さをピクセル単位で返す。水平スクロールバーがあれば、その高さも含める。
4. 上記の関数をイベントリスナーに登録する。
DOMロード完了時とリサイズ、スクロールされたときに実行させる。
//loadで実行
document.addEventListener('DOMContentLoaded', ag2sidebarFix);
//resizeで実行
window.addEventListener('resize', ag2sidebarFix);
//scrollで実行
window.addEventListener('scroll', ag2sidebarFix);
対象要素.addEventListener(‘イベントのタイプ’, ‘関数’, ‘イベント伝播順’) : 対象要素に指定のイベントでコールする関数を指定して登録。第3引数(初期値 : false)でイベントの伝播する方向を指定できる。falseでDOM階層の下位から上位に伝播。
jQueryで実装する場合
同じ挙動をjQueryで記述する場合。
const $sidebar = $('#sidebar'),//サイドバー
$sidebarInner = $('#sidebar-inner'),//サイドバーコンテンツ
$mainInner = $('#main-inner');//メインコンテンツ
const sidebarFixedTop = 'sidebar-fixedtop',
sidebarFixedBottom = 'sidebar-fixedbottom',
sidebarAbsoBottom = 'sidebar-absobottom';
const ag2sidebarClass = {
addFixedTop: function(){
if(!$sidebarInner.hasClass(sidebarFixedTop)){
$sidebarInner.addClass(sidebarFixedTop);
if($sidebarInner.hasClass(sidebarFixedBottom)) $sidebarInner.removeClass(sidebarFixedBottom);
if($sidebarInner.hasClass(sidebarAbsoBottom)) $sidebarInner.removeClass(sidebarAbsoBottom);
}
},
addFixedBottom: function(){
if(!$sidebarInner.hasClass(sidebarFixedBottom)){
$sidebarInner.addClass(sidebarFixedBottom);
if($sidebarInner.hasClass(sidebarFixedTop)) $sidebarInner.removeClass(sidebarFixedTop);
if($sidebarInner.hasClass(sidebarAbsoBottom)) $sidebarInner.removeClass(sidebarAbsoBottom);
}
},
addAbsoBottom: function(){
if(!$sidebarInner.hasClass(sidebarAbsoBottom)){
$sidebarInner.addClass(sidebarAbsoBottom);
if($sidebarInner.hasClass(sidebarFixedTop)) $sidebarInner.removeClass(sidebarFixedTop);
if($sidebarInner.hasClass(sidebarFixedBottom)) $sidebarInner.removeClass(sidebarFixedBottom);
}
},
removeAll: function(){
if($sidebarInner.hasClass(sidebarFixedTop)) $sidebarInner.removeClass(sidebarFixedTop);
if($sidebarInner.hasClass(sidebarFixedBottom)) $sidebarInner.removeClass(sidebarFixedBottom);
if($sidebarInner.hasClass(sidebarAbsoBottom)) $sidebarInner.removeClass(sidebarAbsoBottom);
}
};
let currentY,
sidebarRect,
sidebarInnerRect,
mainInnerRect,
winH,
sidebarH,
sidebarInnerH,
mainInnerH,
sidebarTop,
sidebarBottom;
const ag2sidebarFix = function(flg){
currentY = $(window).scrollTop();
if(flg){
winH = $(window).height(),
sidebarH = $sidebar.outerHeight(),
sidebarInnerH = $sidebarInner.outerHeight(),
mainInnerH = $mainInner.outerHeight();
sidebarTop = $sidebar.offset().top,
sidebarBottom = sidebarTop + sidebarH;
}
if(mainInnerH <= winH || mainInnerH <= sidebarInnerH ){
ag2sidebarClass.removeAll();
}else if(sidebarInnerH <= winH){
if(currentY + sidebarInnerH >= sidebarBottom){
ag2sidebarClass.addFixedBottom();
}else if(currentY > sidebarTop){
ag2sidebarClass.addFixedTop();
}else{
ag2sidebarClass.removeAll();
}
}else{
if(currentY + winH >= sidebarBottom){
ag2sidebarClass.addAbsoBottom();
}else if(currentY + winH > sidebarTop + sidebarInnerH){
ag2sidebarClass.addFixedBottom();
}else{
ag2sidebarClass.removeAll();
}
}
};
$(window).on('load resize', function(){
ag2sidebarFix(true);
});
$(window).on('scroll', function(){
ag2sidebarFix(false);
});
https://memo.ag2works.tokyo/post-2183/
ページをスクロールしてサイドバーが下端まで来たら固定して追従。 | memo メモ [AG2WORKS]
<a href="https://memo.ag2works.tokyo/post-2183/" target="_blank" rel="noopener">ページをスクロールしてサイドバーが下端まで来たら固定して追従。 | memo メモ [AG2WORKS]</a>
この記事へのコメント
コメントの書き込みはまだありません。