CSSのposition: stickyで追従サイドナビを作る。実装して分かった注意点

MANIARUのPROJECT記事では、記事を読みながら右側でSTEP一覧を確認できる追従サイドナビを使っています。

公開したWordPressサイトを実際に使ってUXを改善してみたでは、なぜこのナビが必要になったのかをまとめました。

今回は、CSSのposition: stickyを使ってどう実装したのか、実際に起きたサイドバーの重なりをどう直したのかを技術面から記録します。

追従サイドナビを作りたかった

PROJECT 001「MANIARUを作る。」は、STEP 01からSTEP 14とSPECIAL記事で構成されています。

記事数が増えると、本文を読んでいる途中でも、現在のSTEPとほかの記事を確認できるナビが必要になりました。

PCでは画面右側に余白があるため、そこへPROJECT全体のナビを置き、スクロール中も確認できる形にしました。その追従部分に使ったのがposition: stickyです。

position: stickyの基本

position: stickyを指定した要素は、最初は通常の位置にあります。ページをスクロールして指定した位置まで来ると、そこから一定の位置にとどまります。

position: fixedは最初から画面を基準に固定され、通常のレイアウトから外れます。stickyは親要素の範囲内で動き、もとの配置を保ちながら途中から追従する点が違います。

記事本文と並ぶサイドナビでは、最初の配置を保ちつつ読み進める間だけ追従してほしかったため、stickyを選びました。

最小構成でstickyを実装する

MANIARUの実装を、説明に必要な部分だけへ簡略化すると、HTMLは次のような構造です。

<aside class="project-navigation">
  <a href="/project-001/">PROJECT 001</a>

  <nav class="project-navigation__links">
    <a href="/step-01/">01 CONCEPT</a>
    <a href="/step-02/">02 BRAND & DESIGN</a>
  </nav>
</aside>

実際のサイトでは、PROJECT名、STEP 01〜14、SPECIAL記事、現在位置を示すクラスなども含みます。上のコードは構造を理解しやすくするための説明用です。

追従に必要なCSSも、中心部分だけなら次のようになります。

@media (min-width: 960px) {
  .project-navigation {
    position: sticky;
    top: 92px;
    max-height: calc(100vh - 116px);
    overflow-y: auto;
  }
}

この4つの指定で、上端から少し空けて追従し、ナビが長い場合は内部をスクロールできるようにしています。

固定ヘッダーを考えてtopを決める

stickyでは、どこから追従するかをtopで指定します。MANIARUの現在値は92pxです。

top: 0にすると画面の最上部へ張り付きますが、固定ヘッダーがあるサイトではナビと重なる可能性があります。MANIARUではヘッダーとの間隔を考え、上部に余白を残しました。

92pxはMANIARUのヘッダーとレイアウトに合わせた値です。ヘッダーの高さはサイトごとに異なるため、そのまま共通の正解にはなりません。

長いナビは内部スクロールできるようにする

PROJECT 001は項目が多く、ナビ自体が画面より高くなることがあります。追従しても下の項目へ届かなければ、ナビとして使えません。

そこで、現在は次の2つを組み合わせています。

max-height: calc(100vh - 116px);
overflow-y: auto;

100vhは画面の高さです。そこから上側と下側に必要な余白を引き、ナビの最大高さを決めています。内容がその高さを超えたときだけ、overflow-y: autoによってナビ内部を縦にスクロールできます。

固定したいことより、最後のSTEPまで操作できることを優先した指定です。

stickyは動いたが、通常サイドバーと重なった

最初の実装では、PROJECTナビをWordPressのサイドバー先頭へ挿入しました。sticky自体は意図どおり動きましたが、同じサイドバーには検索や最近の投稿などの通常ウィジェットも残っていました。

PROJECTナビだけが追従し、後ろには別のサイドバー要素が続く構造だったため、スクロールすると専用ナビが通常ウィジェットの上を移動するように見えました。

つまり、今回の原因はstickyが壊れていたことではありません。同じ領域に、PROJECT全体ナビと通常ウィジェットという役割の違う要素を同時に置いていたことでした。

サイドバーの役割を分けて直した

最終的には重なり順だけを変えるのではなく、PROJECT記事と通常記事でサイドバーの内容を分けました。

現在の実装では、PROJECT記事だと判定できた場合だけbodyへ専用クラスを付け、PCのサイドバーではPROJECTナビ以外を表示しません。実際の考え方は次のCSSです。

@media (min-width: 960px) {
  .has-project-navigation
  .sidebar > :not(.project-navigation) {
    display: none;
  }
}

これも実際のクラス名を説明用に短くしています。MANIARUではPROJECT記事だけに条件を付け、通常のカテゴリー記事では通常サイドバーをそのまま表示します。

z-indexで前後関係を変えても、二つの役割が同じ領域に残る点は変わりません。今回は見え方だけを調整せず、どのページに何を表示するかという構造を見直しました。

この修正へ至る過程は、SPECIAL S03 / UX IMPROVEMENTにも残しています。

stickyがうまく動かないときに確認すること

MANIARUで実際に問題になったのは、sticky要素と通常ウィジェットを同じサイドバーへ置いた構造でした。

一般的には、stickyが意図どおり動かない場合、次の点も確認対象になります。

  • stickyをどの要素へ指定したか
  • topが指定されているか
  • 親要素の高さと範囲
  • 親要素や祖先要素のoverflow
  • 固定ヘッダーやほかのsticky要素との位置関係
  • ナビが画面より高くなっていないか
  • レスポンシブCSSで指定が上書きされていないか

これらすべてがMANIARUの原因だったわけではありません。テーマによってDOM構造やCSSが異なるため、まず実際の要素と親子関係を確認する必要があります。

スマートフォンでは別のUIにした

MANIARUでは960px未満の画面でPC用のPROJECTサイドナビを非表示にしています。

代わりに、PROJECT記事の冒頭へdetailsとsummaryを使った折りたたみナビを表示します。現在のSTEP、PROJECTハブ、全STEPとSPECIAL記事を必要なときだけ開く形です。

PCと同じ追従UIを小さな画面へ押し込まず、画面サイズに合う操作へ分けました。折りたたみ部分の詳しい実装は、別の記事で扱える内容として今回は深掘りしません。

実装して分かったこと

position: stickyそのものは、少ないCSSで実装できます。ただし、実際のWordPressサイトでは、固定ヘッダー、親要素、サイドバー内のほかの要素、画面の高さ、モバイル表示まで含めて考える必要がありました。

MANIARUでは、topと最大高さを調整するだけでなく、PROJECT記事ではPROJECTナビ、通常記事では通常サイドバーという役割分担まで決めたことで、追従ナビが安定しました。

ここまでで、考え、制作フローを作り、サイトを改善し、その実装を確認できました。次の段階は、MANIARUを伸ばす実験を始める。まず記録する数字を決めたで始めています。