Posted at

Bootstrap Affixの使い方

More than 1 year has passed since last update.


はじめに

Bootstrapを使ってWebアプリケーションを作る中で、affixを使いたいと思いました。

しかし、Bootstrapサイトにおける説明は率直に言って不足しているし、実例に至っては


The subnavigation on the right is a live demo of the affix plugin.


http://getbootstrap.com/javascript/#affix より引用。以下、引用はすべて同記事より)

などとあるだけで、所望の動作を実現するのに試行錯誤しました。

本稿ではその苦労から学んだaffixの使い方を説明します。

bootstrapのバージョンは3.3.7です。


affixとは


The affix plugin toggles position: fixed; on and off, emulating the effect found with position: sticky;.


position: sticky;がどんなものかはここに仕様と実例が示されています。

position: sticky;があるのにaffixが必要な理由としては以下が挙げられます。


  • 本稿執筆時点でposition: sticky;の実装状況は芳しくない。あるブラウザでは実装されていなかったり、あるブラウザでは一度実装したと思ったらrevertしたりまた実装したりしている


  • positionプロパティの値の1つであるため「普段はabsoluteで位置指定しつつ上端に引っかかったらsticky動作」といったことはできない

  • 動的に生成した要素には適用されない1


affixの使い方

ちょうどposition: sticky;の実例があるので、これをaffixで実装することを目指します。


前準備

position: sticky;は、指定位置に達するまではposition: relative;のように振る舞うので、まずそう書き換えておきます。

dt {

/* 前略 */
position: relative;
top: -1px;
}


1つの要素をaffixする

とりあえず最初のdt要素をaffixしてみます。

affixを使うにはHTML5 data属性を使う方法とjavascriptの2種類ありますが、ここでは前者を用います。ちなみに前者は動的に生成した要素に対して無効です。

data属性で書く場合は要素に対してdata-spy属性に"affix"を与えます。また、affixが発動するオフセット(px)をdata-offset-top属性に与えます。

data-offset-topに与えるのは、対象要素のy座標と固定したいy座標の差です。典型的にはjQuery.offset().topになりますが、ページに固定ヘッダがある場合はその高さだけ減じることになります。

例では以下のようになります。

<dt data-spy="affix" data-offset-top="24">

<!-- 23でないのは、affix発動時にtop: -1pxな状態にしておくため -->

これでaffixが発動しますが、2つ問題が発生します。


affixな要素自身の座標や幅がおかしくなる


you must provide CSS for the positioning and width of your affixed content.


ということで調整します。affixが発動しているときはaffixクラスが付与されるので、それで定義すれば良いでしょう。

.affix {

width: 100%;
}

widthleftがこのように静的に指定できない場合は、affix.bs.affixイベントを利用する必要があるでしょう。


affixな要素の後の要素の座標がおかしくなる

affixな要素はposition: fixed;になるので、元々相対位置指定だった場合は、確保されていたその領域が消滅することになります。したがって、affixしたらその領域を確保しなければいけません。

今回は親要素の上端に、affix要素の分だけpaddingを入れることで解決します。

.affix-affected {

padding-top: 49px; /* 元々のpadding-top 24pxにdt要素の高さを加えたもの */
}

このクラスをどうtoggleするかですが、affixが発動したときにはaffixed.bs.affixイベント、スクロールが上に戻ってaffixが終わったときにはaffixed-top.bs.affixイベントが発動します。これを利用します。

$('dl').on('affixed.bs.affix', function() {

$(this).addClass('affix-affected');
});
$('dl').on('affixed-top.bs.affix', function() {
$(this).removeClass('affix-affected');
});

これで最初のdt要素だけaffix出来ました。

ここまでのjsfiddle https://jsfiddle.net/9b0mdq5q/1/


複数の要素をaffixする

全てのdt要素をaffixするには、やはりjavascriptでやったほうが楽です。

$('dt').each(function() {

var dt = $(this);
dt.affix({
offset: {
top: dt.offset().top + 1 // top: -1px の補正
}
});
});

ここまでのjsfiddle https://jsfiddle.net/o1gmobke/3/


affixを止める位置を指定する

position: sticky;と同様、親要素の下端で止まるようにするにはbottom offsetを指定します。指定する値は「画面下端から、止めたい位置のaffix要素の下端までの距離」です。

ただ、不具合なのかわかりませんが、affix要素にborderやpaddingがあるとその分だけ止める位置がずれるので、それを補正します。

$('dt').each(function() {

var dt = $(this);
var parent = dt.parent();
dt.affix({
offset: {
top: dt.offset().top + 1,
bottom: document.body.clientHeight - (parent.offset().top + parent.outerHeight()) + 4 // 4は縦方向のborderとpaddingの合計
}
});
});

affixが止まるとaffixed-bottom.bs.affixイベントが発火するので、ここでもクラスのtoggleを行います。

affixをとめることにより、複数の要素が同時にaffixすることがなくなったので、dlの親要素のdivで調節するようにしましょう。

.affix-affected {

padding-top: 25px; /* dtの高さ */
}

$('div').on('affixed.bs.affix', function() {

$(this).addClass('affix-affected');
});
$('div').on('affixed-top.bs.affix', function() {
$(this).removeClass('affix-affected');
});
$('div').on('affixed-bottom.bs.affix', function() {
$(this).removeClass('affix-affected');
});

ここまでのjsfiddle https://jsfiddle.net/o1gmobke/4/

余談ですが、affixする要素が元々位置指定されていない場合、affixed-bottomが発動するとtopの他にposition: relative;style属性で指定してくれます。

しかしながらaffixに戻ったときにこのposition: relative;は消えてくれないので消す手間が増えます。


終わりに

bootstrap affixの使い方を、position: sticky;をエミュレーションする過程として説明しました。





  1. 本稿執筆時点のGoogle Chrome 56で確認