#049
posted on 2021.07.08 (Thu)

ページをスクロールしてサイドバーが下端まで来たら固定して追従。

サイトのレイアウトが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要素に任意のクラス名を動的に付与・削除してサイドバーの追従を制御する。

 

  1. スクロールトップのY座標を検知。
  2. 所定のY座標に来たらサイドバーに任意のクラス名を付与・削除。
  3. クラス名に応じてCSSのpositonプロパティーのスタイルが適用される。

 

分岐の条件

表示ウィンドウの高さ、コンテンツの高さによって挙動が異なるので、下記の3通りの条件分岐で対応。

  1. 「メインカラムコンテンツ」の高さが「表示ウィンドウ」または「サイドバーコンテンツ」の高さ以下の場合。
    • サイドバーはスクロールに対し通常の動作を行う。(常に「position: static;」)
  2. 「メインカラムコンテンツ」の高さが「表示ウィンドウ」の高さより大きい場合。
    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);
});
この記事のURL

https://memo.ag2works.tokyo/post-2183/

Copyコピー
この記事のタイトル

ページをスクロールしてサイドバーが下端まで来たら固定して追従。 | memo メモ [AG2WORKS]

Copyコピー
この記事のリンクタグ

<a href="https://memo.ag2works.tokyo/post-2183/" target="_blank" rel="noopener">ページをスクロールしてサイドバーが下端まで来たら固定して追従。 | memo メモ [AG2WORKS]</a>

Copyコピー
※ フィールドをクリックでコピーするテキストの編集ができます。

この記事へのコメント

コメントの書き込みはまだありません。

  • コメント内のタグはエスケープ処理され、文字列として出力されます。
  • セキュリティーのため、投稿者のIPアドレスは取得されます。
  • 管理者が内容を不適切と判断したコメントは削除されます。
  • このフォームにはスパム対策として、Googleの提供するreCAPTCHAシステムが導入されています。
    (Google Privacy Policy and Terms of Service.)