Bootstrap 3 の Affix を今さら勉強する
Bootstrap 3 には Affix という機能が同梱されている。Bootstrap の JS (jQuery プラグイン) を併用して、画面のスクロールに合わせて動的に position: sticky
的な動作を切り替えるような機能だ。
よくウェブサイトのサイドメニューなどで見かける UI だが、自分で作ったことがなかったので、今回試してみた。
目次
- 環境
data
属性を使って指定する基本パターン- 基礎コードのイマイチポイント
- JavaScript (jQuery) で Affix を制御する
- 頑張って可変幅レイアウトに対応させてみる
- 以上
- 参考文献
環境
- jQuery v3.3.1
$.fn.jquery
で確認可能
- Bootstrap 3.3.7
$.fn.tooltip.Constructor.VERSION
で確認可能
これらを適用した単一の HTML を作っていく。
data
属性を使って指定する基本パターン
まずは data-spy="affix"
という data
属性を使用しての基本パターンを実装してみる。
<header style="height: 250px; margin-bottom: 30px; color: #fff; background: #08f;">HEADER</header>
<div class="affix-target-wrapper">
<!--
offset-top : 280 (px) = header 要素の height (250px) + margin-bottom (30px)
offset-bottom : 今回は適当に、画面最下部から 1000px のところで解除する
-->
<div class="affix-target btn-group btn-group-justified" data-spy="affix" data-offset-top="280" data-offset-bottom="1000">
<a href="#" class="btn btn-default">Menu 1</a>
<a href="#" class="btn btn-default">Menu 2</a>
<a href="#" class="btn btn-default">Menu 3</a>
</div>
</div>
<div class="dummy-contents" style="max-width: 800px;">
<!-- ページの高さを稼ぐために適当なダミーテキストを書き連ねておく -->
<p>Lorem ipsum……</p>
</div>
<footer style="height: 400px; color: #fff; background: #444;">FOOTER</footer>
今回は適当に高さのあるヘッダ・フッタと、テキストコンテンツを用意してある。Affix を適用したい要素は .affix-target
で、コレに data-spy="affix"
という属性を振ることで Affix の効果を適用させている。
data-offset-top
の値 (px 単位) が、position: sticky
な挙動に切り替わるスクロール位置となる- 実際は
position: sticky
ではなくposition: fixed
が適用されている - 単純な
position: sticky
と違うのは、「対象要素が画面上部に張り付いたら固定する」だけではなく、任意のスクロール位置で固定を開始できる点
- 実際は
data-offset-bottom
は、data-offset-top
で始めた Sticky を解除する位置。画面最下部からのスクロール位置を指定するdata-offset-bottom
を書かずdata-offset-top
だけ書いた場合は、画面最下部まで行っても Sticky したままとなる- 逆に
data-offset-top
を書かずdata-offset-bottom
だけ指定したい場合は、data-offset-top
に-1
を指定すると、狙った効果を得られる offset-bottom
(.affix-bottom
) で何をしているかというと、position: fixed
を解除しposition: relative
に変更、スクロール量に応じてtop
値を操作することで、その要素がスクロールに追従しているように見せている。position: relative
は対象要素にstyle
属性で埋め込むので、後述する CSS の指定が勝つように指定しないといけない。
Affix を使用する際、対象となる要素に次のような CSS を指定するのが必須となる。ココがドキュメントにちゃんと書かれていなくて、使い方が分かりにくかった。HTML だけ書いても上手く行かないのだ。
/* 子要素 .affix-target が position: fixed になった時、その下にあるコンテンツの表示位置がズレないよう指定しておく */
.affix-target-wrapper {
height: 60px;
}
/* Affix を適用する対象の要素 */
.affix-target {
width: 500px; /* position: fixed になった時、幅が狂わないよう指定しておく */
z-index: 99999; /* position: fixed になった時、他の要素の下に重ならないようにしておく */
}
/* Affix を適用する対象の要素が position: fixed になった時の指定 */
.affix-target.affix {
position: fixed !important; /* .affix 自体に position: fixed 指定はあるものの、.affix-bottom が style 属性に position: relative を埋め込むので、それに勝るよう !important を指定しておく */
top: 0; /* 画面上部からの位置を指定する。data-offset-top との辻褄が合う数値にしておくこと */
}
ついでに、効果を確認するためのデバッグ用に以下を仕込んでおく。
/* Affix の適用状態に応じて文字色を変える */
.affix-top * { color: blue; } /* Affix 開始よりスクロール位置が上の場合 */
.affix * { color: red; } /* position: fixed になった時 */
.affix-bottom * { color: green; } /* Affix を解除するスクロール位置より下の場合 */
/* ページの高さを稼ぐためのコード */
.dummy-contents p {
line-height: 3;
margin-bottom: 3rem;
}
このように実装したサンプルは以下。
- デモ : Practice Bootstrap 3 Affix
- コード : frontend-sandboxes/index.html at master · Neos21/frontend-sandboxes
コレでとりあえず思った動きはできた。
基礎コードのイマイチポイント
上のコードで「それっぽい動き」自体はできたが、イマイチなポイントがある。
- Affix する要素を囲む
.affix-target-wrapper
にheight
を指定しているが、ピクセル固定値で高さを指定するのは難しい場合も多い- Affix する要素が
position: fixed
になることで、後続のコンテンツの表示位置がズレるのを避けるための指定で、CSS の仕様上、height
で高さを稼いでおかないといけないのは理解は出来る。理解は出来るが、イマイチなのだ。
- Affix する要素が
- Affix する要素に
width
を指定しないとposition: fixed
に切り替わった時に幅が狂う- コレも CSS の仕様上、
position: fixed
な要素は画面幅に従うので、理解はできる。しかし、Grid Layout のカラム幅に従わせたい時なんかは、かなり設定が大変だ - Bootstrap 公式リファレンスのサイドバーでも Affix が使われていて、ココは幅が狂わずに表示できている。よくよく実装を見ると、メディアクエリごとに Grid Layout 内のピクセルサイズを定義することで対処していた。こんなのリファレンスで紹介してないじゃん…。
- コレも CSS の仕様上、
position: fixed
を使用するために、可変幅レイアウトとの相性が良くない。ただ Affix を適用しただけではダメで、かなり細かく CSS 側の調整が必要である。
問題点を理解したところで、次のやり方を試してみる
JavaScript (jQuery) で Affix を制御する
続いて jQuery プラグインで制御してみる。
<header style="height: 250px; margin-bottom: 30px; color: #fff; background: #08f;">HEADER</header>
<div class="affix-target-wrapper">
<div class="affix-target btn-group btn-group-justified">
<a href="#" class="btn btn-default">Menu 1</a>
<a href="#" class="btn btn-default">Menu 2</a>
<a href="#" class="btn btn-default">Menu 3</a>
</div>
</div>
<div class="dummy-contents" style="max-width: 400px;">
<!-- ページの高さを稼ぐために適当なダミーテキストを書き連ねておく -->
<p>Lorem ipsum……</p>
</div>
<footer style="height: 500px; color: #fff; background: #444;">FOOTER</footer>
HTML 側は、.affix-target
に指定していた data
属性がなくなっている。
CSS 側は変更なし。JavaScript を次のように追加する。
$(() => {
$('.affix-target').affix({
offset: {
top: $('header').outerHeight(true), // true にすると margin を含めた値を得られる
bottom: 600
}
})
// Affix の適用状態に応じて文字色やフォント設定を変える
.on('affixed-top.bs.affix' , () => { console.log('affixed-top.bs.affix' ); $('.affix-target *').css({ 'color': 'blue' }); })
.on('affixed.bs.affix' , () => { console.log('affixed.bs.affix' ); $('.affix-target *').css({ 'color': 'red' }); })
.on('affixed-bottom.bs.affix', () => { console.log('affixed-bottom.bs.affix'); $('.affix-target *').css({ 'color': 'green' }); })
.on('affix-top.bs.affix' , () => { console.log('affix-top.bs.affix' ); $('.affix-target *').css({ 'font-weight': 'bold' , 'font-style': 'normal' }); })
.on('affix.bs.affix' , () => { console.log('affix.bs.affix' ); $('.affix-target *').css({ 'font-weight': 'normal', 'font-style': 'italic' }); })
.on('affix-bottom.bs.affix' , () => { console.log('affix-bottom.bs.affix' ); $('.affix-target *').css({ 'font-weight': 'bold' , 'font-style': 'italic' }); });
});
久々に書いたぜ jQuery。
このようにすると、data
属性で指定した時と同じ状態が再現できる。.on()
イベントは Affix の状態変化に応じて発生するが、ページ読み込み時は発生しない様子。ちょっと使いづらい感じ。
とりあえず以下のようになる。
- デモ : Practice Bootstrap 3 Affix 2
- コード : frontend-sandboxes/part-2.html at master · Neos21/frontend-sandboxes
頑張って可変幅レイアウトに対応させてみる
JavaScript でも制御できることが分かったら、昔ながらのやり方で、可変幅レイアウトに無理やり対応させることはできそうだ。
まずは HTML。
<header style="height: 250px; margin-bottom: 30px; color: #fff; background: #08f;">HEADER</header>
<div class="container-fluid">
<div class="row">
<div class="col-xs-8">
<div class="dummy-contents">
<!-- ページの高さを稼ぐために適当なダミーテキストを書き連ねておく -->
<p>Lorem ipsum……</p>
</div>
</div>
<div class="col-xs-4">
<ul class="affix-target nav nav-pills nav-stacked">
<li class="active"><a href="#">Menu 1</a></li>
<li><a href="#">Menu 2</a></li>
<li><a href="#">Menu 3</a></li>
<li><a href="#">Menu 4</a></li>
</ul>
</div>
</div>
</div>
<footer style="height: 500px; color: #fff; background: #444;">FOOTER</footer>
.container-fluid
を使用して、可変幅にしている。Affix したい .affix-target
は、Grid Layout 用の .col-xs-4
の直下にいる。.affix-target-wrapper
はなくなった。
続いて CSS。
/* Affix を適用する対象の要素 */
.affix-target {
z-index: 99999; /* position: fixed になった時、他の要素の下に重ならないようにしておく */
/* width を指定していない */
}
/* Affix を適用する対象の要素が position: fixed になった時の指定 */
.affix-target.affix {
position: fixed !important; /* .affix 自体に position: fixed 指定はあるものの、.affix-bottom が style 属性に position: relative を埋め込むので、それに勝るよう !important を指定しておく */
top: 30px; /* 画面上部からの位置を指定する。data-offset-top との辻褄が合う数値にしておくこと */
}
/* Affix の適用状態に応じて文字色を変える */
.affix-top * { color: blue; } /* Affix 開始よりスクロール位置が上の場合 */
.affix * { color: red; } /* position: fixed になった時 */
.affix-bottom * { color: green; } /* Affix を解除するスクロール位置より下の場合 */
/* ページの高さを稼ぐためのコード */
.dummy-contents p {
line-height: 3;
margin-bottom: 3rem;
}
.affix-target
に書いていた width
指定がなくなった。あとレイアウト調整のために affix-target.affix
で指定する top
値を調整した。
そして JavaScript。
$(() => {
$('.affix-target').affix({
offset: {
top : $('header').outerHeight(false),
bottom: $('footer').outerHeight(true) + 400
}
})
.on('affix.bs.affix', () => {
// .affix-target の親要素の、padding の内側の幅を取得して、それを .affix-target の幅とする
$('.affix-target').width($('.affix-target').parent().width());
})
.on('affix-top.bs.affix affix-bottom.bs.affix', () => {
// .affix-top・.affix-bottom 時は上で指定した幅指定を解除し、親要素の幅に従わせる
$('.affix-target').width('auto');
});
// ウィンドウリサイズに対応する
$(window).resize(() => {
// リサイズによる文章の折り返しなどでスクロール量が変わった場合に向けて、Affix の状況を再計算させる
$('.affix-target').affix('checkPosition');
// Affix 適用中の .affix-target のみ指定し、横幅を動的に設定する
$('.affix-target.affix').width($('.affix-target').parent().width());
});
});
.affix('checkPosition')
という関数を使い、Affix の再計算をさせたりして、.affix-target
の親要素の内側の幅に合わせるよう、動的に width
を変更している。
コレで以下のようになる。
- デモ : Practice Bootstrap 3 Affix 3
- コード : frontend-sandboxes/part-3.html at master · Neos21/frontend-sandboxes
多分よくあるデザインだと、これくらいのことはしないといけないのかなと思われる。
以上
最近は Angular や Vue などの SPA で開発することが多いので、このように jQuery にベッタリな実装だとつらいモノがある。当時はまだ普及していなかった position: sticky
という「標準」ももう整備されたし、今後はあまり使うことはないのかな。
ただ、特定のスクロール位置で position: sticky
を解除する動きは、今も CSS オンリーでは困難な動きなので、こうした動きを再現したい場合は position: relative
と top
値の動的変更が必要になってくる (もしくは position: absolute
に切り替えてスクロール量に合わせた top
値を指定するとか・いずれもリサイズに対応できないと微妙になる)。
Affix の挙動を理解し、ノウハウを吸収できたので、あとは必要に応じて自分で実装してみるとしよう。
参考文献
- JavaScript · Bootstrap
- Bootstrap 2.1「Affixプラグイン」がわかりにくかったので少し紐解いてみました | hijiriworld Web
- アフィックス ≪ JavaScript ≪ Bootstrap3日本語リファレンス
- Bootstrap Affix
- http://algo13.net/bootstrap/affix.html
- Bootstrap3のAffixとScrollspyを試してみた | while(isプログラマ)
- How to Use Bootstrap 3 Affix Plugin - Tutorial Republic
- リストの幅が変更されたブートストラップ3.0接辞 | 翻訳QAサービス | code adviser