2021-1-14

position: fixedなヘッダーがアンカーリンク先の要素と被る問題の対策

雑な技術メモ

追記:2021-1-14

CSSの進化によって面倒が少なくなったので修正した

参考ツイート

結論

  • スムーススクロールはJSではなくCSSの scroll-behavior: smooth を使用する。
  • ヘッダーに被らないようにするには
    • Safari or IE11対応必要
      • :target 疑似セレクタを使用する
    • Safari and IE11対応不要
      • scroll-margin-top を使用する

scroll-behavior

ルート要素に scroll-behavior: smooth を指定することで、スクロール挙動がスムースになる。スクロール速度に関しては自動で設定されるため関与できない。「もうちょっとスムーススクロール速くしたい」と言われたら頑張って説明しよう。

JSでスムーススクロールをやってしまうと、JSとCSS両方でヘッダー分ずらす処理を入れないといけないため二重管理となってしまいつらい。これを避けるために極力このプロパティを使うべき。

ヘッダーに被らないようにするには

scroll-margin-top というセレクタがあり、これを指定するとCSS制御のスクロールの際に指定した値分だけずらしてくれる。

:root {
  --headerHeight: 100px;
}

:target {
  scroll-margin-top: var(--headerHeight);
}

上のようにすれば、これだけでヘッダーが被らないようになる。便利。

ただし、SafariとIE11は非対応のためこれら2つに対応する場合。

:target:before {
  content: '';
  display: block;
  height: var(--headerHeight);
  margin-top: var(--headerHeight * -1);
  visibility: hidden;
}

のようにする必要がある点に注意。

以下過去記事


position: fixedなヘッダーがある場合、何も対策していないとスムーススクロールの際にヘッダーとスクロール先の要素が被ってしまう。

そういう場合に一般的には以下のようにヘッダーの高さ分スクロール位置をずらすことで対策をしている。

var duration = 300
var href = $(this).attr('href')
var $target = $(href === '#' || href === '' ? 'html' : href)

if ($target.length === 0) return false

var headerHeight = 100

// ヘッダーの高さ分スクロール位置をずらすことで対策
var position = $target.offset().top - headerHeight

$('html,body').stop().animate({ scrollTop: position }, duration, 'swing')

ただこの対策しかしていない場合、<a href="./about.html#message">のようにページ外からのアンカーリンクに関してはヘッダーがだだ被りしてしまう。ページ外からの遷移の場合は前述の対策が適用されないので当然。

対策方法

DEMO

アンカーリンクのターゲットとなる要素に対して、以下の CSS を適用する。

:before {
  content: '';
  display: block;

  margin-top: -100px;
  padding-top: 100px;
}

margin-top及びpadding-topの値はヘッダーの高さ。

このスタイルの与え方は以下の2パターンある。

:targetを利用する方法

Demo の Example01 の方法

:not(.js-scroll-target):target:before {
  content: '';
  display: block;

  margin-top: -100px;
  padding-top: 100px;
}

:targetはハッシュの対象となっている要素。こうしておけばページ外からのリンクの場合だけ対象の要素に対してスタイルを適用することが出来る。

:not(.js-scroll-target)は、ページ外からハッシュ付きで遷移した後さらにスムーススクロールをした場合にヘッダーがずれてしまうことに対する対策。スムーススクロールをする前のタイミングで$target.addClass('js-scroll-target')とかしてこのスタイルが適用されないようにするとずれがなくなる。

既存サイトの場合、出来るだけ今あるソースコードに手を入れずに修正したい場合が多々あるのでそういう場合この方法が便利。

普通にクラスを与える方法

Demo の Example02 の方法

.u-anchorTarget:before {
  content: '';
  display: block;

  margin-top: -100px;
  padding-top: 100px;
}

すごい普通な感じ。このu-anchorTargetクラスを愚直にアンカーリンクの対象となる要素に与える。

クラスを与える手間はあるが、スムーススクロールの際にヘッダーの高さ分 JS でスクロール位置をずらしたり、:targetの方法のようにスクロール前に特定のクラスを付与したりする必要がない。

ヘッダー被りの対策コードがこのスタイルただ1点に集中するため後から見た際にコードの意図がわかりやすい…と思う。