Android版のgoogle chromeでは、アドレスバーが上部に配置されていますが、ページを下にスクロールするとこれが隠れるように設計されています。
今回は、この動きをjavascriptで再現してみたいと思います。
デモページ
実装
デザイン変更を考慮し、可能な限り変更箇所を少なくするよう心がけます。
考慮すべきこと
- スクロール時のパフォーマンス
- CSS改修時の変更コスト
考慮しないこと
- リロード時の対応
- IE対応
- 途中で止めた場合の動作
それでは、次項で解説していきます。
解説
まずはHTMLを記述します。簡単なもので構いません。
今回は以下のようにしました。
<body>
<nav id='slidable-nav'>
<p>nav</p>
</nav>
<article>
<h1>your contents</h1>
<p>The quick brown fox jumps over the lazy dog.</p>
<p>The quick brown fox jumps over the lazy dog.</p>
<p>The quick brown fox jumps over the lazy dog.</p>
</article>
</body>
ナビゲーションバー部分とアーティクル(記事)部分の単純なものです。
次に、CSSを記述します。
<style type='text/css'>
body{
height:2000px;
}
#slidable-nav{
background-color:rgba(0,0,0,0.5);
width:100%;
position:fixed;
top:0;
left:0;
}
</style>
こちらもわかりやすいように最低限のスタイル付けを行います。
次に、本体であるjavascript部分をコーディングしていきます。
<script>
(function(){
document.addEventListener("DOMContentLoaded",function(){
var FPS = 60;
var previousScrollPosition = window.pageYOffset;
var nav = document.getElementById("slidable-nav");
var navHeight = nav.clientHeight;
var minTop = - navHeight;
var id = null;
document.body.style.paddingTop = (parseInt(document.body.style.paddingTop || 0) + navHeight) + "px";
window.addEventListener("scroll",function(){
clearTimeout(id);
id = setTimeout(function(){
var y = window.pageYOffset;
var amount = previousScrollPosition - y;
var navY = (parseInt(nav.style.top) || 0);
var absolute = navY + amount;
absolute = Math.min(absolute,0);
absolute = Math.max(absolute,minTop);
nav.style.top = absolute + "px";
previousScrollPosition = y;
},1000/FPS);
});
});
})();
</script>
非常に単純ですが、順に見ていきましょう。
グローバル汚染を防ぐため、即時関数で囲みます。
更に、DOMContentLoadedにイベントリスナを追加します。
(function(){
document.addEventListener("DOMContentLoaded",function(){
ここで、よくloadイベントに登録しているコードを見かけますが、DOMContentLoaded
がDOMツリーの生成後すぐに呼ばれるのに対し、loadイベントでは、画像など全ての要素がロードされて初めて呼ばれます。
そのため、ページ閲覧者に優れたユーザビリティを提供できない場合が多く、通常loadイベントを使うべきではありません。特段の理由が無い限りは、DOMContentLoadedイベントを登録しましょう。
次に、使用する変数を定義します。
var FPS = 60;
var previousScrollPosition = window.pageYOffset;
var nav = document.getElementById("slidable-nav");
var navHeight = nav.clientHeight;
var minTop = - navHeight;
var id = null;
変数名 | 説明 |
---|---|
FPS | setTimeoutの間隔 |
previousScrollPosition | 前回のスクロール位置を記憶しておくための変数 |
nav | ナビゲーションバーのDOMオブジェクト |
navHeight | ナビゲーションバーの高さ |
minTop | ナビゲーションバーのMin位置 |
id | setTimeoutのID |
次に、スクロール位置が一番上のときにコンテンツが隠れないよう、bodyのpadding-topを設定します。
document.body.style.paddingTop = (parseInt(document.body.style.paddingTop || 0) + navHeight) + "px";
通常、paddingTopプロパティはIntでなく、”○○px”といったStringで格納されています。
javascriptのparseIntは、整数変換のためのメソッドですが、pxなどの余分な文字列を排除して数値化してくれるという、素晴らしい仕様となっています。
しかし、空文字を評価した場合はNaNを返しますので、NaNが返ってきた場合は0を返し、更にnavHeight分を加えてpadding-topを設定します。
次に、イベントリスナを追加します。
window.addEventListener("scroll",function(){
おなじみですね。スクロールにフックして関数を呼びます。
ここで直接スタイルの変更処理を書いてもいいのですが、スクロールイベント毎に関数を呼ぶとパフォーマンス的な問題があったりするので、一工夫加えます。
clearTimeout(id);
id = setTimeout(function(){
setTimeoutを使い、遅延させることで、一定時間以内に連続してイベントが発火した場合は、最後のイベントのみ呼ばれるようにします。
var y = window.pageYOffset;
var amount = previousScrollPosition - y
var navY = (parseInt(nav.style.top) || 0);
var absolute = navY + amount;
pageYOffsetで現在のスクロール位置を取得し、記憶していたスクロール位置から現在のスクロール位置を引いた差分をとり、amount(移動量)を求めます。
次に、ナビゲーションの現在のtop位置を取得し、amountを足して絶対位置を求めます。この絶対位置が、設定するtop値になります。
そのままtopに設定できれば良いのですが、そうするとスクロールした分だけ無限にtop値が変化してしまいます。そのため、変化する上限を設定する必要があります。
if文を使っても三項演算子を使っても構いません。自分がやりやすい方法で実装します。
absolute = Math.min(absolute,0);
absolute = Math.max(absolute,minTop);
今回は、Math.min、Math.maxでお茶を濁します。
絶対位置が求められたら、それを設定するだけですね。
nav.style.top = absolute + "px";
previousScrollPosition = y;
前回スクロール位置の記憶用変数に、今回のスクロール位置を忘れず設定します。
},1000/FPS);
setTimeoutの第二引数は、ミリ秒で評価されますので、1000/FPSの値を設定します。
これで、簡易ではありますが、スクロールにあわせて画面に吸い付くようにナビゲーションが表示されるようになりました。
工夫は必要ですが、更にパフォーマンスをよくすることもできますし、途中で止めた場合は完全に表示したり、逆に完全に非表示にできたりもします。
ユーザビリティを考える
ところで、今回実装したようなナビゲーションは、見栄えがよくてかっこいいのですが、スマートフォンサイトなどでは、貴重な画面の領域を食いつぶしてしまう一因となります。
文章主体のサイトでは、少し戻って文章を読み直したい場合もあるでしょう。
たとえば、おしえてgooという有名なサイトでは、上に少しでもスクロールすると、アニメーション等も殆どなく、パっとグローバルナビが出現します。こういった実装は、必要なコンテンツの閲覧を阻害してしまったり、突然現れることによってストレスを感じてしまうユーザーも多いことでしょう。
コンテンツは、ユーザーに向けて発信されるものであって、開発者の独りよがりになってはいけません。
今回紹介した実装方法も、閲覧する環境によっては、スクロールがカクついたりなどの弊害があるかもしれません。例えば、ある一定以上の移動量になったらスクロールを行うなどで応急的な処置は可能だと思いますが、これらの障壁を全て取り除くことは不可能です。
うまく妥協点を見つけつつ、ユーザビリティの優れたページを作っていきたいですね。