メインコンテンツ内だけを追従するサイドメニューの実装

メインコンテンツ内だけを追従するサイドメニューの実装

スクロールに追従するサイドメニューの実装方法です。

スクロールに追従する動きはサイドメニューだけでなく、ECサイトの商品ページなどで常に購入エリアを表示させながら商品説明を読んでもらう、図面を常に表示させながら図の説明を読んでもらうなど、使う場面が意外にあります。

メインコンテンツエリアをスクロールするときだけ追従するので、メインコンテンツより上では、メインコンテンツの一番上で止まり、メインコンテンツ内では追従、メインコンテンツより下ではメインコンテンツの一番下で止まるような仕様です。

制作したのは、サイドメニューが動きありで追従するパターン(パターンA)とサイドメニューが動きなしでぴったり追従するパターン(パターンB)です。

制作物

パターンA(サイドメニューが動きありで追従するパターン)

See the Pen fixed aside with animation by takblog (@blanks-site) on CodePen.

パターンB(サイドメニューが動きなしでぴったり追従するパターン)

See the Pen fixed aside by takblog (@blanks-site) on CodePen.

パターンBのほうがサイドメニューのボリュームが多く、画面からはみ出してる場合はスクロール可能です。これはパターンAでも同じ挙動になります。

サイドメニューにスクロールバーを出したくない場合は、下記の記事を参考にスクロールバーを非表示にしてください。

スクロールバーを非表示にするスクロールバーを非表示にする

パターンA版コード

パターンA版のhtml,css,javascriptのコードです。

htmlとcssは分かりやすくするために最低限のことしか記述していません。

html

コードをクリップボードにコピー
<!-- ↑↑↑ヘッダーとか -->
<div id="container">
  <main>メインコンテンツエリア</main>
  <aside id="aside">サイドメニューエリア</aside>
</div>
<!-- ↓↓↓フッターとか -->

css

コードをクリップボードにコピー
#container{
  position: relative;
}
#aside{
  width: 150px;/* サイドメニューの幅 */
  position: absolute;
  left: 0;
  top: 0;
  max-height: calc(100vh - 20px); /* offsetY分をマイナス */
  overflow: auto;
}
#aside.is-end{
  top: auto;
  bottom: 0;
}

javascript

コードをクリップボードにコピー
(function(){
  const container = document.getElementById('container'); // コンテンツエリアを囲む要素
  const aside = document.getElementById('aside');         // サイドメニュー要素
  const offsetY = 20;  // ピッタリ上にくっつかないように少し余白を持たせる
  container.style.minHeight = aside.clientHeight + 'px'; 
  window.addEventListener('scroll',()=>{
    const containerRect = container.getBoundingClientRect();
    const isReachBottom = (aside,containerRect,offsetY)=>{
      if( aside.clientHeight < window.innerHeight ){
        if(containerRect.bottom <= aside.clientHeight + offsetY){
          return true;
        }else{
          return false;
        }
      }else{
        if(containerRect.bottom < window.innerHeight - offsetY){
          return true;
        }else{
          return false;
        }
      }
    }

    // ↓↓↓↓↓図で説明↓↓↓↓
    if( isReachBottom(aside,containerRect,offsetY) ){
      // ①メインコンテンツの一番下までスクロールしたとき
      aside.style.top = '';
      aside.classList.add('is-end');
    }else if( containerRect.top < offsetY ){
      // ②メインコンテンツ内をスクロールしているとき
      aside.classList.remove('is-end');
      const y = containerRect.top*-1 + offsetY;
      aside.style.top = y+'px';
    }else{
      // ③メインコンテンツより上をスクロールしているとき
      aside.classList.remove('is-end');
      aside.style.top = '';
    }
  });
})();

コード内の「図で説明」箇所を簡単に図で説明します。

コード内の「offsetY」「y」「containerRect.top」はそれぞれ図の中に示している通りです。

offsetYはwindowの上にピッタリくっつくのを防ぐための値です。0にすればwindowの上にぴったりくっつきます。

次の図はコード内の①と②の境目の図です。

サイドメニューがcontainerの一番下に来た時には、「is-end」クラスをサイドメニュー要素に追加して、containerの一番下で止まるようにしています。

①と②の境目の図

次の図は②と③の境目の図です。

windowの一番上がcontainerの一番上よりoffsetY分だけ上に来た時に、②の状態に移行します。

②と③の境目の図

パターンB版コード

パターンB版のhtml,css,javascriptのコードです。

パターンB版もhtmlとcssは分かりやすくするために最低限のことしか記述していません。

htmlはパターンAと同じものです。

html

コードをクリップボードにコピー
<!-- ↑↑↑ヘッダーとか -->
<div id="container">
  <main>メインコンテンツエリア</main>
  <aside id="aside">サイドメニューエリア</aside>
</div>
<!-- ↓↓↓フッターとか -->

css

コードをクリップボードにコピー
#container{
  position: relative;
}
#aside{
  width: 150px; /* サイドメニューの幅 */
  position: absolute;
  left: 0;
  top: 0;
  max-height: calc(100vh - 20px); /* offsetY分をマイナス */
  overflow: auto;
}
#aside.is-fixed{
  position: fixed;
  top: 20px; /* offsetY */
}
#aside.is-fixed.is-end{
  position: absolute;
  top: auto;
  bottom: 0;
}

javascript

コードをクリップボードにコピー
(function(){
  const container = document.getElementById('container'); // コンテンツエリアを囲む要素
  const aside = document.getElementById('aside');         // サイドメニュー要素
  const offsetY = 20;  // ピッタリ上にくっつかないように少し余白を持たせる
  container.style.minHeight = aside.clientHeight + 'px'; 
  window.addEventListener('scroll',()=>{
    const containerRect = container.getBoundingClientRect();
    const isReachBottom = (aside,containerRect,offsetY)=>{
      if( aside.clientHeight < window.innerHeight ){
        if(containerRect.bottom <= aside.clientHeight + offsetY){
          return true;
        }else{
          return false;
        }
      }else{
        if(containerRect.bottom < window.innerHeight - offsetY){
          return true;
        }else{
          return false;
        }
      }
    }

    // ↓↓↓↓↓図で説明↓↓↓↓
    if( isReachBottom(aside,containerRect,offsetY) ){
      // ①メインコンテンツの一番下までスクロールしたとき
      aside.classList.add('is-end');
      aside.classList.add('is-fixed');
      aside.style.left = '';
    }else if( containerRect.top < offsetY ){
      // ②メインコンテンツ内をスクロールしているとき
      aside.classList.add('is-fixed');
      aside.classList.remove('is-end');
      aside.style.left = containerRect.left + 'px';
    }else{
      // ③メインコンテンツより上をスクロールしているとき
      aside.classList.remove('is-fixed');
      aside.classList.remove('is-end');
      aside.style.left = '';
    }
  });
})();

コード内の「図で説明」箇所を簡単に図で説明します。

パターンAと違うのは②の状態で、サイドメニューはposition:fixed;が適用されるようになるところです。

position:fixedが適用されるので、left:0;だとwindowの一番左にくっついてしまいます。

それを防ぐために「containerRect.left」を取得して、サイドメニューのleftに適用しています。

パターンAと同じようにposition:absolute;のままtopを変化させてもいいのですが、ガタつくことが多いので、fixedにしています。

①と②の境目の図の説明はちら、②と③の境目の図の説明はちらから確認してください。

arrow_circle_up