スクロール量によって要素を固定する
サイドバーが消えてしまう
最近は1カラムサイトが流行っていますので、2カラム、3カラムサイトを作る機会は減っているでしょうか?しかしやはり2カラム以上のサイトも需要はあると思います。
記事などのページの場合、サイドバーよりメインカラムのheight
が長く、スクロールするとサイドバーが画面からスクロールアウトしてしまう事が多々あります。せっかくサイドバーにコンテンツを置いているのに、空白になってしまうのはとてももったいないです。
そこでサイドバーの終端に行きついたら、サイドバーを固定する処理を施してみようと思います。このことをスクロールアンカーなどと言うそうです。固定というよりアンカーの方がちょっとかっこいいかもしれないです。
jQuery
でもwordpress
でもこの手の機能を付加するプラグインがありますので、手っ取り早く実現したい場合はそれらを使う手もありますが、例によってこのサイトでは下手でも自分で作ってみようと思います。自作派の方の参考になれば幸いです。
jvascript
だけでもできるのですが、今回はjQuery
を併用することにしましました。以下の様な構造を想定して話を進めたいです。
固定するにはposition:fixdeを使う
positionプロパティ
サイドバーを固定するには、CSS
にてposition:fixde
を指定する必要があります。position
は要素の配置方法を設定するプロパティで、fixde
はその名の通り「固定」を表し、position:fixde
を設定するとスクロールしてもその要素はその場に留まり続けます。
position
のデフォルト値はstatic
で、特にposition:static
を設定していなくても最初からstatic
になっています。static
は配置基準を通常にします。 static
が設定されているときは位置指定プロパティのtop,left,bottom,right
は効かないです。
サイドバーをスクロールに追従させるときはposition:static
になっている必要があります。(relative
でもいいのだがちょっと面倒なので無視する)
スクロールがサイドバーの終端にさしかかったら、サイドバーのCSS
をposition:static
からposition:fixde
にスイッチ、スクロールがサイドバーの終端から内側に入ったらposition:fixde
からposition:static
にスイッチさせるという動作でサイドバー固定は実現できそうです。
また忘れてはいけないのがフッターの存在です。フッターが画面内に現れてくるタイミングで、またサイドバーをスクロールさせるようにしなくてはなりません。fixed
のままだとフッターにサイドバーが貫入してしまいます。これを防ぐためにposition:absolute
を使う。
スクロール量の取得
.scrollTop()を使う
今回の動作はスクロールが基準になるので、スクロール量の取得をしなければ話になりません。取得にはjQuery
の.scrollTop()
メソッドを使う。要素に対してのスクロール量を取得するメソッドです。例えばbody
のスクロール量を取得したければ以下の様に記述します。
$("body").scrollTop();
値はピクセル単位ですが、単位名が付いたりはせず数値として取得できるので計算に使う場合は別途加工する必要はないです。
.scrollTop()の位置
.scrollTop()
は指定した要素の上辺の位置を取得することになります。その為スクロールエンド時の.scrollTop()
で取得できる数値は構成要素のheight
とは一緒になりません。window
のheight
分少なくなります。
今回は.scrollTop()
ライン(画面上辺)ではなく、画面下辺ラインを判断基準にする必要が出てきます。 画面下辺ラインがサイドバー終端に到達したら…、フッタートップに到達したら…という条件づけで固定、追従を切り分けるからです。
画面下辺ラインを得るには以下の様にします。
var bottom_scroll_line = $( "body" ).scrollTop() + $( window ).height();
ブラウザによる差異
普通body要素
は要素の大本親として扱える場合が多く、ページ全体のスクロール量を取得する場合はbody要素
を指定すればよさそうですが、一筋縄ではいかないです。
非常に面倒なことに、ブラウザによる差異があるからです。すべてのブラウザで試したわけではないですが、少なくともIE
とchrome
とfireFox
では値の取得に差が現れる事は確かめましました。
端的にいうとIE
とfireFox
では$("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();
}
///以下何らかの処理
});
この関数はスクロール時に発動されます。 つまりこの関数が実行されているということはスクロール量が発生している状態だということです。だからbody
かhtml
どちらかで値の取得ができる状況のはずです。とりあえず上記ではとりあえず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().top
はmargin
を考慮していないようだ(完全に確かめたわけではないですが、少なくともchrome
ではmargin
分ズレる原因となった)。
要素の足し合わせでも用を足すので今回は.offset().top
での位置取得は行わないです。
position:fixde時の位置に対しての準備/h2>
配置位置を補正する
position:fixde
は何も設定しなければ、画面座標(0.0)に要素の左上が留まることになります。それを意図して使う場合は何ら問題ないですが、今回に関してはそれでは困ます。 position:fixde
にしたとたんにサイドバーの位置がずれてしまうからです。
位置がずれないように適切な処置をしてあげる必要があります。その為にサイドバーを設置している方向の他の要素のmargin,padding
を取得することになります。
サイドバーを取り巻く環境はサイトの作り方によってバラバラなので、取得するべき要素は状況によって異なりますが、基本的にはサイドバーの親要素クラスに設定してあるmargin,padding
の総量を取得するということです。
サイドバー親要素のmargin,padiing,borderの総量
図ではかなりシンプルにcontainer要素
の中にsidebar
が存在している状況を想定しています。container要素
にはmargin
かpadiing
かもしくはその両方が設定されていますので、サイドバーはwindow
の最右辺には接せず、container要素
のmargin-right
とpadiing-right
の総量分左側に寄っていることになります。
position:fixde
になった場合にもwindow
の最右辺に対して、container要素
のmargin-right
とpadiing-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
が存在するので、通常であれば#container
のpadding
を除いたwidth
が#sidebar
のwidth
の計算根拠になります。しかしfixed
をした場合、#sidebar
のwidth
の計算根拠はおそらくwindow
に移行するのではないかと思います。
そうするとpadding
分計算根拠が変わるわけで、#sidebar
のwidth
にも影響がでるという訳です。
念の為WC3
のCSS仕様書
を読んでみたが、以下の様に書かれていましましました。
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.
意訳するなら
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" );
}
}
});
コードを確認するとき合わせて以下の図も参照していただけるといいかもしれないです。状態遷移のイメージ図です。
補足説明
位置の補正に関して
17行目でright
というプロパティを使っていますが、これが前述した位置補正です。right
は要素の位置を設定するプロパティで「画面最右辺を基準にして指定数値分左に動かす」ことができます。
これに#container
のpadding-right
の値を当ててやれば、fixed
しても元の位置になるので位置補正が出来るという訳です。
またbottom
プロパティに0
を設定していますが、これを設定しないとfixed
になった時、サイドバーの上辺が画面上辺にfixされるので、縦方向で位置がずれてしまいます。その為、画面下辺にfixさせるためbottom
に0
を設定し、「下から0pxの位置」
を確保しています。
position:absoluteの位置計算根拠について
footer
が画面内に現れるタイミングでposition:absolute
を適用しています。これはサイドバーがfooterに重ならないよう、fixed
からスクロールに復帰させるため処理です。
position:absolute
はposition:static
以外の値が設定された最も近い先祖要素を基準とします。 今回の例では#container
にrelative
を設定してありますので、position:absolute
になった瞬間に#sidebar
は#container
を基準に位置を確保しようとします。
しかし、fixed
の時にはwindow
を基準として位置を確保しようとするので、fixed→absolute
(その逆も)に遷移する段階で計算根拠が変わるので、位置ズレの原因になります。
今回の例では#container
にはpadding
を設定してありますので、position:absolute
になった場合、window
とはpadding
分基準位置が変わり、結果縦方向にズレる原因となります。bottom
にfooter
のheight
だけでなく、#container
のpadding-bottom
も加えているのはそういった理由からです。
今回の例では#container
にrelative
を設定しなければ、それ以上の要素(bodyやhtml)にはrelative
を設定していないので、fixed→absolute
になっても計算根拠に変更は生じないです。
しかし、position
はかなり多用されるプロパティなので、他の処理との兼ね合いでどうしても設定しなければならないこともあるでしょう。ズレの原因がわかりにくくなる嵌りポイントなので注意が必要です。
その他の補足
今回はサイドバーが右にありますので、right
プロパティで位置補正を行ったが、サイドバーが左側ならleftプロパティを使ってほしいです。
#container
のpadding-right
の取得にparseFloat()
を使っていないですが、これは取得値を計算する必要がなく"px"
が付いている状態そのままでright
プロパティに当てても問題がないからです。
まとめ
えらい長い記事になってしまい、飽き飽きされたのではないかと思うが堪忍していただきたいです。サイトユーザはクリックよりもスクロールすることを厭わないという調査結果があるそうで、スクロールまわりの機能は重要度が増してくるかもしれないです。
今回はまた@MINOの無能さを露呈している記事
ですが、少しは参考になれば幸いです。もっとうまいやり方はいくらでもあると思う
ので、ぜひ挑戦していただきたいです。
毎度のことながら間違いがあったらごめんなさいです。