2016/09/02 16:18:16

サイドバーの終端でスクロールを固定させる方法(スクロールアンカー)

目次(クリックするとジャンプします)
  • 1:スクロール量によって要素を固定する
  • 1.1:サイドバーが消えてしまう
  • 2:固定するにはposition:fixdeを使う
  • 2.1:positionプロパティ
  • 3:スクロール量の取得
  • 3.1:.scrollTop()を使う
  • 3.2:.scrollTop()の位置
  • 3.3:ブラウザによる差異
  • 4:サイドバーの高さの取得
  • 4.1:.outerHeight()
  • 4.2:サイドバーの上にある要素の高さを取得
  • 4.3:.offset().topではだめなのか
  • 5:position:fixde時の位置に対しての準備/h2>
  • 5.1:配置位置を補正する
  • 5.2:サイドバー親要素のmargin,padiing,borderの総量
  • 5.3:サイドバーのwidthを固定値に
  • 6:スクロールアンカーの例
  • 6.1:一連のコードにしてみた
  • 7:補足説明
  • 7.1:位置の補正に関して
  • 7.2:position:absoluteの位置計算根拠について
  • 7.3:その他の補足
  • 8:まとめ

スクロール量によって要素を固定する

サイドバーが消えてしまう

最近は1カラムサイトが流行っていますので、2カラム、3カラムサイトを作る機会は減っているでしょうか?しかしやはり2カラム以上のサイトも需要はあると思います。

記事などのページの場合、サイドバーよりメインカラムのheightが長く、スクロールするとサイドバーが画面からスクロールアウトしてしまう事が多々あります。せっかくサイドバーにコンテンツを置いているのに、空白になってしまうのはとてももったいないです。

そこでサイドバーの終端に行きついたら、サイドバーを固定する処理を施してみようと思います。このことをスクロールアンカーなどと言うそうです。固定というよりアンカーの方がちょっとかっこいいかもしれないです。

jQueryでもwordpressでもこの手の機能を付加するプラグインがありますので、手っ取り早く実現したい場合はそれらを使う手もありますが、例によってこのサイトでは下手でも自分で作ってみようと思います。自作派の方の参考になれば幸いです。

jvascriptだけでもできるのですが、今回はjQueryを併用することにしましました。以下の様な構造を想定して話を進めたいです。

sidebar-3

固定するにはposition:fixdeを使う

positionプロパティ

サイドバーを固定するには、CSSにてposition:fixdeを指定する必要があります。positionは要素の配置方法を設定するプロパティで、fixdeはその名の通り「固定」を表し、position:fixdeを設定するとスクロールしてもその要素はその場に留まり続けます。

positionのデフォルト値はstaticで、特にposition:staticを設定していなくても最初からstaticになっています。staticは配置基準を通常にします。 staticが設定されているときは位置指定プロパティのtop,left,bottom,rightは効かないです。

サイドバーをスクロールに追従させるときはposition:staticになっている必要があります。(relativeでもいいのだがちょっと面倒なので無視する)

スクロールがサイドバーの終端にさしかかったら、サイドバーのCSSposition:staticからposition:fixdeにスイッチ、スクロールがサイドバーの終端から内側に入ったらposition:fixdeからposition:staticにスイッチさせるという動作でサイドバー固定は実現できそうです。

また忘れてはいけないのがフッターの存在です。フッターが画面内に現れてくるタイミングで、またサイドバーをスクロールさせるようにしなくてはなりません。fixedのままだとフッターにサイドバーが貫入してしまいます。これを防ぐためにposition:absoluteを使う。

スクロール量の取得

.scrollTop()を使う

今回の動作はスクロールが基準になるので、スクロール量の取得をしなければ話になりません。取得にはjQuery.scrollTop()メソッドを使う。要素に対してのスクロール量を取得するメソッドです。例えばbodyのスクロール量を取得したければ以下の様に記述します。

$("body").scrollTop();

値はピクセル単位ですが、単位名が付いたりはせず数値として取得できるので計算に使う場合は別途加工する必要はないです。

.scrollTop()の位置

.scrollTop()は指定した要素の上辺の位置を取得することになります。その為スクロールエンド時の.scrollTop()で取得できる数値は構成要素のheightとは一緒になりません。windowheight分少なくなります。

今回は.scrollTop()ライン(画面上辺)ではなく、画面下辺ラインを判断基準にする必要が出てきます。 画面下辺ラインがサイドバー終端に到達したら…、フッタートップに到達したら…という条件づけで固定、追従を切り分けるからです。

画面下辺ラインを得るには以下の様にします。

var bottom_scroll_line = $( "body" ).scrollTop() + $( window ).height();

ブラウザによる差異

普通body要素は要素の大本親として扱える場合が多く、ページ全体のスクロール量を取得する場合はbody要素を指定すればよさそうですが、一筋縄ではいかないです。

非常に面倒なことに、ブラウザによる差異があるからです。すべてのブラウザで試したわけではないですが、少なくともIEchromefireFoxでは値の取得に差が現れる事は確かめましました。

端的にいうとIEfireFoxでは$("body").scrollTop()でのスクロール量取得は出来ないです。その為$("body").scrollTop()ではなく$("html").scrollTop()にてスクロール量を取得する必要があります。しかしchrome$("html").scrollTop()でスクロール量を取得する事が出来ないです。

$(window).scrollTop()$(document).scrollTop()に関しても同様で、取得できるブラウザとできないブラウザがあります。

だからスクロール量を取得するには$("body").scrollTop()$("html").scrollTop()を切り替える必要があります。対応したコードを記載しておこうと思います。

$( window ).on( "scroll", function(){

    var scroll;

    //判別の為とりあえず$( "body" ).scrollTop()で取得してみる
    var scroll_distinction = $( "body" ).scrollTop();
    //body要素でスクロールを取得できているか
    //出来ていませんでしましました。ら"html"出来ていたら"body"
    if( scroll_distinction === 0 || scroll_distinction == false ){
        scroll = $( "html" ).scrollTop();
    }else{
        scroll = $( "body" ).scrollTop();
    }

    ///以下何らかの処理
});

この関数はスクロール時に発動されます。 つまりこの関数が実行されているということはスクロール量が発生している状態だということです。だからbodyhtmlどちらかで値の取得ができる状況のはずです。とりあえず上記ではとりあえずbodyで取得してみて値が得られるかを確かめています。

値が得られていれば$( "body" ).scrollTop()でそうでなければ、$( "html" ).scrollTop()で取得する仕組みとなっています。じゃあ$( "body" ).scrollTop()でも$( "html" ).scrollTop()でも取得できない場合はどうするのか?

おそらくそんなブラウザは無いはずです。$( "body" ).scrollTop()$( "html" ).scrollTop()のどちらかで確実に値を取得できると断言しておこう(無責任)。

サイドバーの高さの取得

.outerHeight()

さて今度はサイドバーの高さを取得する必要があります。サイドバーの終端を特定するためです。これは.outerHeight()で取得したいです。.outerHeight()padding,borderを含むheightを取得できます。 もしサイドバーの外殻にmarginを設定してあるなら.outerHeight(true)とすることでmarginを含めたheightを取得できます。

.outerheight()ではなく.outerHeight()。普通の.height()は全部小文字ですが、.outerHeight()は大文字のHになっているところがあるので注意。これで嵌った記憶があります。

サイドバーの最外殻の要素が#sidebarだとするならば以下の記述でheightが取得できるはずです。

//padding,borderを含める場合
var sidebar_height = $( "#sidebar" ).outerHeight();
//marginも含める場合
var sidebar_height = $( "#sidebar" ).outerHeight( true );

サイドバーの上にある要素の高さを取得

図で示したようにサイドバーの上にはヘッダーなどの要素が乗っかっていることが多いです。このサイドバーの上にある要素の高さも取得する必要があります。

この高さを取得してサイドバーの高さと合わせて条件にしないと、サイドバーが中途半端なところで固定されてしまうからです。

//padding,borderを含める場合
var sidebar_height = $( "header" ).outerHeight();
//marginも含める場合
var sidebar_height = $( "header" ).outerHeight( true );

図では上に乗っかっているのはヘッダーだけですが、もし他にも乗っかっているものがあれば、その要素分のheightも取得する必要があります。

.offset().topではだめなのか

要素を足し合わすより、目的の要素の位置を直接取得するという手も考えましました。.offset().topが使えるのではないかと思ったのですが、2つの理由で却下しましました。

一つはtop,bottom等の位置要素を指定すると取得値に変化が出てしまうからです。

もう一つは、どうやら.offset().topmarginを考慮していないようだ(完全に確かめたわけではないですが、少なくともchromeではmargin分ズレる原因となった)。

要素の足し合わせでも用を足すので今回は.offset().topでの位置取得は行わないです。

position:fixde時の位置に対しての準備/h2>

配置位置を補正する

sidebar-1

position:fixdeは何も設定しなければ、画面座標(0.0)に要素の左上が留まることになります。それを意図して使う場合は何ら問題ないですが、今回に関してはそれでは困ます。 position:fixdeにしたとたんにサイドバーの位置がずれてしまうからです。

位置がずれないように適切な処置をしてあげる必要があります。その為にサイドバーを設置している方向の他の要素のmargin,paddingを取得することになります。

サイドバーを取り巻く環境はサイトの作り方によってバラバラなので、取得するべき要素は状況によって異なりますが、基本的にはサイドバーの親要素クラスに設定してあるmargin,paddingの総量を取得するということです。

サイドバー親要素のmargin,padiing,borderの総量

図ではかなりシンプルにcontainer要素の中にsidebarが存在している状況を想定しています。container要素にはmarginpadiingかもしくはその両方が設定されていますので、サイドバーはwindowの最右辺には接せず、container要素margin-rightpadiing-rightの総量分左側に寄っていることになります。

position:fixdeになった場合にもwindowの最右辺に対して、container要素margin-rightpadiing-rightの総量分は左に寄せる(最右辺から離す)処理をしなければなりません。

これらの値の取得は以下のような記述で行う。

//サイドバーが右にあり、containerにmarginだけの場合
var total_amount = parseFloat( $( "#container" ).css( "margin-right" ) );

//サイドバーが右にあり、containerにmarginだけの場合
var total_amount = parseFloat( $( "#container" ).css( "margin-right" ) );

//サイドバーが右にあり、containerにmarginとpaddingがある場合
var total_amount = parseFloat( $( "#container" ).css( "margin-right" ) ) + parseFloat( $( "#container" ).css( "padding-right" ) );

もしサイドバーが左側にあるなら、margin-left,padding-leftで取得します。 parseFloat()というメソッドを使っているがこれはjavascriptのメソッドで、型変換をするためのものです。

.css()で値を取得すると、値の単位まで付いてくる文字列としての取得になってしまうので、そのままでは計算に使えないです。その為、parseFloat()をつかって文字列を数値に変換して(たとえば20pxの20を取り出すような感じ)計算に使えるようにします。

あまりないと思いますが、もし親要素にboderを設定してあるならそれも取得して総量に加える必要があります。

var total_amount = parseFloat( $( "#container" ).css( "boder-right-width" ) );

ともかくサイドバー側の画面辺からサイドバーまでの幅を全部取得しましょう。

取得した値での実際の位置補正は後述します。

サイドバーのwidthを固定値に

アンカーする対象の要素のwidth%で指定されていると、fixdeしてright,leftプロパティを設定した時点でwidthが変わってしまうようです。これはおそらくfixdeを設定した場合のwidthの計算根拠に起因するのでないかと思います。

例では#containerの子として#sidebarが存在するので、通常であれば#containerpaddingを除いたwidth#sidebarwidthの計算根拠になります。しかしfixedをした場合、#sidebarwidthの計算根拠はおそらくwindowに移行するのではないかと思います。

そうするとpadding分計算根拠が変わるわけで、#sidebarwidthにも影響がでるという訳です。

念の為WC3CSS仕様書を読んでみたが、以下の様に書かれていましましました。

If the element has 'position: fixed', the containing block is established by the viewport in the case of continuous media or the page area in the case of paged media.

意訳するなら

「もし要素に’position: fixed’が設定されているなら親要素(containing block)はwindowになるぜ、ナンシー?」

continuous mediaとかpaged mediaとかよくわからないですが、ともかくfixedでは親要素がwindowになると思っておけばよさそうです。

これを回避するにはサイドバーのwidthを実数値にする必要があります。具体的なpx値指定であれば親要素が変わっても問題ないです。以下の様にしてサイドバーのwidthを計算することができます。

$( "#sidebar" ).css( "width" , ( $( "#container" ).width() - $( "#main" ).outerWidth( true ) ) );

スクロールアンカーの例

一連のコードにしてみた

長々と駄文を連ねてしまったが、今まで説明してきたことを念頭にスクロールアンカーをコーディングしてみましましました。

思ったより短いコードになりましました。

$( window ).on( "scroll ready resize" , function(){
                var scroll;
                //メイン要素がサイドバーより短い場合はアンカーしない
                if( $( "#main" ).outerHeight() >= $( "#sidebar" ).outerHeight()){
                    //各種数値の取得
                    var window_height = $( window ).height();
                    var body_height = $( "body" ).outerHeight();
                    var header_height = $( "header" ).outerHeight( true );
                    var main_height = $( "#main" ).outerHeight( true );
                    var sidebar_height = $( "#sidebar" ).outerHeight( true );
                    var footer_height = $( "footer" ).outerHeight( true );
                    var container_padding_right = parseFloat( $( "#container" ).css( "padding-right" ) );
                    var container_padding_bottom = parseFloat( $( "#container" ).css( "padding-bottom" ) );

                    //判別の為とりあえず$( "body" ).scrollTop()で取得してみる
                    var scroll_distinction = $( "body" ).scrollTop();
                    //body要素でスクロールを取得できているか
                    //出来ていませんでしましました。ら"html"出来ていたら"body"
                    if( scroll_distinction === 0 || scroll_distinction == false ){
                        scroll = $( "html" ).scrollTop();
                    }else{
                        scroll = $( "body" ).scrollTop();
                    };

                    //スクロール量を基準に固定、追従を切り分ける
                    if(  scroll < window_height || scroll < ( header_height + sidebar_height ) - window_height ){
                        $( "#sidebar" ).css( "position" , "static" );
                    }else if( scroll >= ( header_height + sidebar_height ) - window_height && scroll < ( body_height - window_height ) - footer_height ){
                        $( "#sidebar" ).css( "position" , "fixed" ).css( "right" , container_padding_right ).css( "bottom" , "0" );
                        $( "#sidebar div" ).css( "color" , "#FFD44B" );
                    }else if( scroll >= ( body_height - window_height ) - footer_height && scroll <= body_height - window_height){
                        $( "#sidebar" ).css( "position" , "absolute" ).css( "right" , container_padding_right ).css( "bottom" , footer_height + container_padding_bottom );
                        $( "#sidebar div" ).css( "color" , "#000000" );
                    }
                }
            });

コードを確認するとき合わせて以下の図も参照していただけるといいかもしれないです。状態遷移のイメージ図です。

sidebar-2

補足説明

位置の補正に関して

17行目でrightというプロパティを使っていますが、これが前述した位置補正です。rightは要素の位置を設定するプロパティで「画面最右辺を基準にして指定数値分左に動かす」ことができます。

これに#containerpadding-rightの値を当ててやれば、fixedしても元の位置になるので位置補正が出来るという訳です。

またbottomプロパティに0を設定していますが、これを設定しないとfixedになった時、サイドバーの上辺が画面上辺にfixされるので、縦方向で位置がずれてしまいます。その為、画面下辺にfixさせるためbottom0を設定し、「下から0pxの位置」を確保しています。

position:absoluteの位置計算根拠について

footerが画面内に現れるタイミングでposition:absoluteを適用しています。これはサイドバーがfooterに重ならないよう、fixedからスクロールに復帰させるため処理です。

position:absoluteposition:static以外の値が設定された最も近い先祖要素を基準とします。 今回の例では#containerrelativeを設定してありますので、position:absoluteになった瞬間に#sidebar#containerを基準に位置を確保しようとします。

しかし、fixedの時にはwindowを基準として位置を確保しようとするので、fixed→absolute(その逆も)に遷移する段階で計算根拠が変わるので、位置ズレの原因になります。

今回の例では#containerにはpaddingを設定してありますので、position:absoluteになった場合、windowとはpadding分基準位置が変わり、結果縦方向にズレる原因となります。bottomfooterheightだけでなく、#containerpadding-bottomも加えているのはそういった理由からです。

今回の例では#containerrelativeを設定しなければ、それ以上の要素(bodyやhtml)にはrelativeを設定していないので、fixed→absoluteになっても計算根拠に変更は生じないです。

しかし、positionはかなり多用されるプロパティなので、他の処理との兼ね合いでどうしても設定しなければならないこともあるでしょう。ズレの原因がわかりにくくなる嵌りポイントなので注意が必要です。

その他の補足

今回はサイドバーが右にありますので、rightプロパティで位置補正を行ったが、サイドバーが左側ならleftプロパティを使ってほしいです。

#containerpadding-rightの取得にparseFloat()を使っていないですが、これは取得値を計算する必要がなく"px"が付いている状態そのままでrightプロパティに当てても問題がないからです。

まとめ

えらい長い記事になってしまい、飽き飽きされたのではないかと思うが堪忍していただきたいです。サイトユーザはクリックよりもスクロールすることを厭わないという調査結果があるそうで、スクロールまわりの機能は重要度が増してくるかもしれないです。

今回はまた@MINOの無能さを露呈している記事ですが、少しは参考になれば幸いです。もっとうまいやり方はいくらでもあると思うので、ぜひ挑戦していただきたいです。

毎度のことながら間違いがあったらごめんなさいです。