twitter-bootstrap の固定ナビゲーションバー(navbar-fixed-top)を使っている時に、ページ内アンカーでジャンプすると、固定ナビゲーションバーの太さの分だけ上部が隠れてしまう問題に遭遇したのでメモっておきます。

CSS はこんなかんじ。

.topbar {
   height: 40px;
   position: fixed;
   top: 0;
   left: 0;
   right: 0;
   z-index: 10000;
   overflow: visible;
}

body {
  padding-top: 40px;
}
ぐぐると色々な記事にヒットするのですが、どうやらまだオフィシャルに解決方法を提供していないようですね。StackOverflow の記事で見つけた css だけで解決する方法など色々と試したところ、最終的には github.com/twitter/bootstrap の Issue になっていたこちらのページの以下の方法が役に立ちました。

https://github.com/twitter/bootstrap/issues/1768#issuecomment-13306753
<head>
  <script>
    var shiftWindow = function() { scrollBy(0, -50) };
    window.addEventListener("hashchange", shiftWindow);
    function load() { if (window.location.hash) shiftWindow(); }
  </script>
</head>
<body onload="
load()"> 

この javascript コードにより、ページ内アンカーリンクをクリックした場合、およびアンカー付きのURLに直接飛んで来た場合に、ナビゲーションバーに表示が隠れることがなくなりました。動作的には、飛んだ後に scrollBy で微調整スクロールしているかんじですね。

これで完成か、と思ったのですが、試してみたところ chrome, firefox では動作するのですが、safari でうまく動作しないんですよね。safari のバージョンは 6.0.3 でした。

どうも、ページ内アンカーリンクをクリックしてジャンプする時の scrollBy 的な動作と、この javascript で書いている scrollBy の動作が競合しておかしな動きになっているような気配でした。試しに
<head>
  <script>
    var shiftWindow = function() { setTimeout("scrollBy(0, -50)", 200) };
    window.addEventListener("hashchange", shiftWindow);
    function load() { if (window.location.hash) shiftWindow(); }
  </script>
</head>
<body onload="load()">  

と setTimeout を付けて少し待たせてから scrollBy するようにするときちんと動作しました。じゃあ、それで解決?というわけではなく、この待ち時間のために、ちょっと待ってからピコッ!と移動するような動きになって UX 的に大変いけていないのがなんとかしたい。

最終的には jquery で次のように書くことで、期待通りの動作をしてくれるようになりました。
$(function() {
  $(window).hashchange(function(){
    var pos = $("a[name='" + location.hash.substr(1) + "']").offset().top;
    // $('html, body').animate({ scrollTop: pos - 40 }, 0); // does not jump well on page load on safari
    // $('html, body').animate({ scrollTop: pos - 40 }, 1); // works, but have to wait 1ms
    window.scrollTo(0, pos - 40); // worked on safari, chrome, firefox well
  });
  // trigger the event on page load.
  if (location.hash) { $(window).hashchange(); }
});

scrollBy (相対座標) ではなく scrollTo (絶対座標) の関数を使うことで、safari でも期待通りに動くようになるようです。その場合、アンカー先の絶対座標を取得する必要があるわけですが、素の javascript でそれをやろうとすると、element の親の親の親の親 .... と辿って全ての offsetTop 値を足し合わせる必要があって大変辛いので、その辺をすでにやってくれている jquery の offset() メソッドを使って書いています。

jquery なのだから、window.scrollTo() ではなく、jquery の animate() メソッドを使おうとしたのですが、そちらだと 1ms (かそれ以上)の待ち時間を追加しないとまた safari でまともに動作してくれず、setTimeout の時と同じくピコッ!と移動するような動きになってしまって嫌だったので、window.scrollTo をそのまま使うことにして落ち着きました。

ブラウザによる挙動の違いとか全く腑に落ちていないので何かツッコミあればぜひ。