スクロールしても固定されて追尾するサイドバー

最終更新日
2017.10.12

スクロールしても付いてくるサイドバーを自分なりに使いやすく作ってみました。

(2017/10)flexbox 前提で少し作り直しました。もしカクっとなる場合は教えてください。

$(document).ready() だと実行するタイミングが早くて、画像などの内容の読み込みが終わった状態の要素の縦幅を取得できない場合があるので $(window).load() を使うのがいいようです。

サイドバーの上基準に固定

flexboxメインコンテンツとサイドバーの高さが同じになっている事前提なので、もし float の場合は予め JavaScript で高さを同じにしてください。

固定中のマージンも含めてレイアウトは全て CSS で出来るようにしてあり、ラッパー・ナビゲーションの CSS に margin padding border の指定があっても JavaScript の調整なしで動くようになっています。

固定レイヤーは CSS の margin を含んだ状態で位置が固定されます。左のコンテンツと下の位置が合うようにするには、サンプルのように marginpadding で調整してください。

  • シンプルに上固定するタイプ
  • ラッパーよりナビゲーションの方が大きい場合は何もしない

デモページ

var fixedNavigation = (function () {
  var navi, wrap, startFixScroll, endFixScroll, endFixScrollPos
  return {
    run: function () {
      // naviが可動する範囲のラッパー要素
      wrap = $('.side')
      // 固定する要素
      navi = $('.fixnav')
      this.refresh()
    },
    refresh: function () {
      navi.css({
        position: 'relative',
        top: 'auto'
      })
      // マージンを除いた要素のTOP位置
      var wrapTop = wrap.offset().top
      var naviTop = navi.offset().top
      // ラッパーは下内側の位置
      var checkWrapBottom = wrapTop + wrap.outerHeight()
        - parseInt(wrap.css('padding-bottom'))
        - parseInt(wrap.css('border-bottom-width'))
      var checkNaviBottom = naviTop + navi.outerHeight(true)
        - parseInt(navi.css('margin-top'))
      // ラッパーに収まる
      if (checkWrapBottom > checkNaviBottom) {
        // A 固定開始位置 navi の top
        startFixScroll = naviTop - parseInt(navi.css('margin-top'))
        // B ラッパーより下がった場合
        endFixScroll = checkWrapBottom - navi.outerHeight(true)
        // B の時に固定する位置 wrapper - navi を引いた高さ
        endFixScrollPos = wrap.innerHeight() - parseInt(wrap.css('padding-bottom')) - navi.outerHeight(true)
        $(window).off('scroll.fixnav', _onScroll).on('scroll.fixnav', _onScroll)
      } else {
        $(window).off('scroll.fixnav', _onScroll)
      }
      $(window).trigger('scroll')
    }
  }

  function _onScroll() {
    var ws = $(window).scrollTop()
    if (ws > endFixScroll) {
      // ラッパーより下
      navi.css({
        position: 'absolute',
        top: endFixScrollPos + 'px'
      })
    } else if (ws > startFixScroll) {
      // 固定中間
      navi.css({
        position: 'fixed',
        top: '0px'
      })
    } else {
      // 固定開始まで
      navi.css({
        position: 'relative',
        top: 'auto'
      })
    }
  }
})()

$(window).on('load', function () {
  fixedNavigation.run()
})

最初スクリプト側でマージンをつけていましたが、あとから弄るときに面倒臭そうだったので画面端に固定して CSS 側で調整するようにしました。

サイドバーの下を基準に固定

上固定だとサイドバーが長い時一番下までスクロールしないと見えないので下固定にしたもの。

  • 画面からはみ出る場合下を基準に固定、画面より小さい場合は上を基準に固定
  • 折りたたみなどで動的に長さが変わった場合あとから簡単に補正

デモページ

var fixedNavigation = (function () {
  var navi, wrap, startFixScroll, startFixScrollPos, endFixScroll, endFixScrollPos
  return {
    run: function () {
      // 固定する要素
      navi = $('.fixnav')
      // naviが可動する範囲のラッパー要素
      wrap = $('.side')
      this.refresh()
    },
    refresh: function () {
      navi.css({
        position: 'relative',
        top: 'auto'
      })
      var windowHeight = $(window).height()
      // マージンを除いた要素のTOP位置
      var wrapTop = wrap.offset().top
      var naviTop = navi.offset().top
      // ラッパーは下内側の位置
      var checkWrapBottom = wrapTop + wrap.outerHeight()
        - parseInt(wrap.css('padding-bottom'))
        - parseInt(wrap.css('border-bottom-width'))
      var checkNaviBottom = naviTop + navi.outerHeight(true)
        - parseInt(navi.css('margin-top'))
      // ラッパーに余白がある
      if (checkWrapBottom > checkNaviBottom) {
        // ウィンドウより大きい=下固定
        if (windowHeight < navi.outerHeight(true)) {
          // A 固定開始位置 navi の top
          startFixScroll = checkNaviBottom - windowHeight
          startFixScrollPos = windowHeight - navi.outerHeight(true)
          // B ラッパーより下がった場合
          endFixScroll = checkWrapBottom - windowHeight
          // B の時に固定する位置 wrapper - navi を引いた高さ
          endFixScrollPos = wrap.innerHeight() - parseInt(wrap.css('padding-bottom')) - navi.outerHeight(true)
        } else {
          // A 固定開始位置 navi の top
          startFixScroll = naviTop - parseInt(navi.css('margin-top'))
          startFixScrollPos = 0
          // B ラッパーより下がった場合
          endFixScroll = checkWrapBottom - navi.outerHeight(true)
          // B の時に固定する位置 wrapper - navi を引いた高さ
          endFixScrollPos = wrap.innerHeight() - parseInt(wrap.css('padding-bottom')) - navi.outerHeight(true)
        }
        $(window).off('scroll.fixnav', _onScroll).on('scroll.fixnav', _onScroll)
      } else {
        $(window).off('scroll.fixnav', _onScroll)
      }
      $(window).trigger('scroll')
    }
  }

  function _onScroll() {
    var ws = $(window).scrollTop()
    if (ws > endFixScroll) {
      // ラッパーより下
      navi.css({
        position: 'absolute',
        top: endFixScrollPos + 'px'
      })
    } else if (ws > startFixScroll) {
      // 固定中間
      navi.css({
        position: 'fixed',
        top: startFixScrollPos + 'px'
      })
    } else {
      // 固定開始まで
      navi.css({
        position: 'relative',
        top: 'auto'
      })
    }
  }
})()

$(window).on('load', function () {
  fixedNavigation.run()
})

ウィンドウや要素の高さが変わった場合はそのタイミングで fixedSidebar.refresh(); すると補正します。

レスポンシブにも対応させようかと思ったんですが、スマホにこの機能は必要なさそうなので fixedNavigation.run(); 付近でごにょごにょ処理するぐらいでもいいかも。