cssプロパティのposition: sticky
を使用したときに上手く動作せず嵌ったので、その解決法についてまとめた。
今回嵌ったposition: sticky
当サイトでは、ヘッダー部分でposition: sticky
を使用していた。ヘッダー内には、ボタンクリックでメニューが開閉するハンバーガーメニューを置いている。
構造は以下のとおり(※一部簡略化)。
<body>
<header role="banner" class="header">
<div class="header-inner">
...
<nav role="navigation" class="nav">
<button>
<span class="bar"></span>
<span class="sr-only">MENU</span>
</button>
<ul class="list">
<li class="list-item">HOME</li>
<li class="list-item">Post</li>
<li class="list-item">About</li>
</ul>
</nav>
</div>
</header>
<main class="main">
...
</main>
<footer class="footer">
...
</footer>
</body>
ヘッダー(.header
)にposition: sticky
を設定。初期状態ではメニュー(.list
)を隠しておき、ボタンクリックで右側からスライドして表示されるという仕組みになっている。
.header {
height: 40px;
position: sticky;
top: 0;
}
.list {
position: fixed;
z-index: 100;
inset: 0 -100% 0 100%;
transition: transform 0.4s;
display: grid;
gap: 20px;
place-content: center;
text-align: center;
min-height: 100dvh;
}
inset: 0 -100% 0 100%
で、メニューを画面外(右)に追いやっている。
これでヘッダーはスクロールすると画面上に追随するようになり、メニューはボタンクリックで右からスライドしてくるようになった…と思われた。
ここで2つの問題が発生した。
画面が横スクロールする
ウィンドウ下を見ると横スクロールバーが表示されていた。メニューが画面外(右)に隠れているのかと思いきや、コンテンツ横に幅100%状態で並んで画面幅が計200%になってしまっている。
スマホ版でposition: sticky
が効かない
PC版は追随してくれるが、スマホ版は一瞬固定されてもすぐに剥がれてコンテンツと一緒にスクロールされる。
ちなみに、メインコンテンツの高さを調整するためにbody
にdisplay:grid
を指定しているが、body
に対してdisplay:grid
やdisplay:flex
を適用することは可能で、今回の不具合とは無関係である。
解決法
あくまでposition: sticky
で指定したい場合
position: sticky
が適用されないのは、そもそもスティッキーコンテナが存在していないことが原因であった。
私はbody
をdiv
やsection
のように親として扱えると思っていたが、body
はスティッキーコンテナにすることはできない。スマホ版で、スティッキーアイテムが一瞬固定されてもすぐ剥がれてしまうのはこのためだと思われる(PC版で効いていた理由は不明だが)。
position:sticky
を使う場合は、body
配下にページ全体を囲むdiv
を設けてこれをスティッキーコンテナにする。
<body>
<div class="wrapper">
<header role="banner" class="header">
...
</header>
<main class="main">
...
</main>
<footer class="footer">
...
</footer>
</div>
</body>
横スクロール問題は、スティッキーコンテナ(.wrapper
)にoverflow-x: clip
をかけることで解決する。overflow: hidden
でも良さそうに思えるが、スティッキーアイテムの親・祖先要素にoverflow: hidden
を指定するとposition: sticky
が効かなくなってしまう。
overflow: clip
も使いたくない場合は、いっそ中のコンテンツをスティッキーアイテムから出した方がいいかもしれない。
position: fixed
に変更する
スタイリングのためだけに余計なDOMを増やしたくない場合は、いっそsticky
をやめて従来のfixed
に書き換えるという手もある。
.header {
height: 40px;
position: fixed;
top: 0;
width: 100%;
}
.main {
margin-top: 40px;
}
fixed
の場合は、右に追いやったメニューがスクロールで出てくる現象は発生しないので、親要素にoverflow
プロパティを指定する必要はない。その代わり、.header
に幅を指定し、次の要素のmargin-top
に.header
の高さ分を指定する必要がある。
position: sticky
とposition: fixed
の違い
なぜ、position: sticky
の場合に画面外のコンテンツ分のスクロールが発生したのか。position: sticky
とposition: fixed
の違いについて少し調べてみた。
position: sticky
Sticky positioning, which visually shifts a box relative to its laid-out location in order to keep it visible when a scrollable ancestor would otherwise scroll it out of sight.
CSS Positioned Layout Module Level 3
「スティッキーポジショニングは、レイアウトされた位置から視覚的に相対ボックスを移動させ、スクロール可能な祖先要素がスクロールで見えなくなっても、ボックスを表示し続ける」
sticky
は、relative
同様にstatic
に(親要素のボックス配置の文脈に従って)相対的に配置される。
position: fixed
Fixed positioning, which absolutely positions the box and affixes it to the viewport or page frame so that it is always visible.
CSS Positioned Layout Module Level 3
「フィックスドポジショニングは、ボックスが常に見えるように、ビューポートまたはページフレームに対して絶対的に配置、貼り付けられる」
一方のfixed
は、absolute
のようにフローから外れて絶対配置される。フローから外れるということはつまり、fixed
指定されたボックスの幅と高さは消失する。
sticky
はフローからは外れないので、.header
の高さ・幅は消失しない。そのため、初期状態で画面右に追いやったはずのリストが画面スクロールで出てくるといった現象が発生したようだ。
まとめ
position: sticky
が指定されたスティッキーアイテムはstatic
に配置される。幅・高さは失われずその領域は保たれる。スティッキーアイテム内のコンテンツをposition: fixed
で画面外に配置すると、スティッキーアイテムからはみ出す。position: sticky
を使う場合は、body
配下にスティッキーコンテナとなるdiv
を設ける。position: sticky
ではみ出したコンテンツを隠す場合は、overflow: hidden
ではなくoverflow: clip
を指定するか、コンテンツをスティッキーアイテム外に配置する。- DOMを増やしたくない場合は、
position: fixed
で指定する。
追記
あなたが教わってるそのCSSテクニックはもう古い | TAKLOG
上の記事「画面いっぱいのメインビジュアルを実装するなら高さの値に 100svb(100svh) を使いなさい」の項では、body
をスティッキーコンテナにして.footer
要素を最下部に固定する方法が紹介されており、実際に上記ブログで.header
.footer
の固定化が実現されている。
しかし、当ブログでは同じように設定して(したつもりで)も再現できない。恐らく何らかの条件が異なるからだと思われるが…(要検討)。