TT Lab
はじめる
学ぶ 学習パス コース

CSS — 後に書いた規則が勝つ

原因はいつも祖先にある

TT Labで続きを見る

一言でいうと

fixedが画面に固定されず、stickyがついてこないとき、原因はその要素ではなく祖先にあります。包含ブロックとスクロールコンテナーを誰が作ったのかを探せば、答えが出ます。

なぜ必要なのか

この2つのバグは、症状が原因をまったく指し示しません。

どちらも、その要素のCSSは完璧に正しいです。開発者ツールでその要素だけを見ても、何も問題がありません。そのため、人は値を変えながら何時間も使います。見るべき場所は祖先です。

どう動くのか: 包含ブロック

top、left、width: 50%のような値は、すべて包含ブロック(containing block)を基準に測ります。その包含ブロックが何なのかは、positionの値が決めます。MDNの包含ブロックのドキュメントが、ルールを整理しています。

position 包含ブロック
static・relative 最も近いブロックの祖先のcontent領域
absolute 最も近い位置指定された(staticではない)祖先のpadding領域
fixed ビューポート
sticky フローの基準は親、固定される基準は最も近いスクロールコンテナー

ここまでは、たいていの人が知っています。問題は、その先です。

fixedを閉じ込める属性

祖先のどれか1つでも次の属性を持つと、その祖先がfixedとabsoluteの新しい包含ブロックになります。

transform          (none 이 아닌 값)
filter
backdrop-filter
perspective
will-change: transform | filter | perspective
contain: layout | paint | strict | content

そのため、こういうことが起きます。カードにマウスを重ねたときに少し浮き上がる効果を付けようとして、transform: translateY(-2px)を入れます。あるいは、パフォーマンスのためにwill-change: transformを入れます。その瞬間、カードの中にあったモーダルのオーバーレイがカードの中に閉じ込められます。アニメーションとモーダルは無関係に見えるため、原因を見つけるまでに時間がかかります。

CSS Positioned Layout 3とCSS Containment 3が、それぞれの規定を載せています。

stickyが固定されない2つの理由

position: stickyは、2つの条件がどちらも合って初めて動作します。

1つ目は、しきい値が必要なことです。top、right、bottom、leftのうち1つ以上がautoではない必要があります。1つもなければ、固定される線がないので、そのまま流れていきます。エラーも警告もありません。

2つ目は、スクロールコンテナーが実際にスクロールされる必要があることです。stickyは、最も近いスクロールコンテナーの中でだけ固定されます。スクロールコンテナーは、overflowがvisibleではない最も近い祖先です。

ここでoverflow: hiddenが事故を起こします。横スクロールを防ぐために、途中のコンテナーにoverflow-x: hiddenを入れると、overflow-yも一緒にautoになり、そのボックスがスクロールコンテナーになります。ユーザーはそのボックスをスクロールせず(ページをスクロールします)、stickyが固定される場面が永遠に来ません。

そして、stickyが固定される範囲は、親ボックスの中です。親が自分の高さ分しかなければ、その分だけついてきます。「少しついてきて終わる」という症状は、ほとんどいつもこれです。

z-indexはどのボックスの中で競うのか

z-index: 9999を指定したのに、背後に敷かれてしまうことがあります。数字が小さいからではなく、別のスタッキングコンテキストの中にあるからです。

スタッキングコンテキストは、次のようなものが作ります。MDNのスタッキングコンテキストのドキュメントが、全体の一覧を載せています。

祖先がスタッキングコンテキストを作ると、その中のz-indexはそのコンテキストの中でだけ通用します。親がz-index: 1で兄弟がz-index: 2なら、親の中の子が9999でも、その兄弟の下になります。opacity: 0.99のような何気ない1行が、これを作ります。

現場での姿

3つのことが、繰り返し出てきます。

1つ目は、ドロップダウンが親に切り取られることです。祖先にoverflow: hiddenがあり、ドロップダウンがabsoluteです。fixedに変えればよいのですが、その祖先にtransformがあると、fixedも閉じ込められます。根本的な解決は、メニューをドキュメントの最上部へ移すこと(ポータル)です。

2つ目は、モバイルでだけヘッダーが固定されないことです。狭い画面で横方向のはみ出しを防ぐために、overflow-x: hiddenを入れたメディアクエリがあったのです。

3つ目は、iOSでだけおかしいことです。これはたいてい、-webkit-overflow-scrollingやセーフエリア(env(safe-area-inset-*))の問題ですが、ここで扱うルールとは別の軸なので、実機で確認する必要があります。推測で直さないほうがよいです。

診断の順番を覚えておけば、ほとんどが5分以内に終わります。

  1. この要素のpositionは何か
  2. 包含ブロックは何か: 祖先をたどって上がりながら、transform系を探す
  3. stickyなら、しきい値はあるか、スクロールコンテナーはどれか
  4. z-indexなら、祖先のうち誰がスタッキングコンテキストを作ったか

次のラボですること

決まった文書構造にスタイルシートを書き、包含ブロックとスクロールコンテナーを計算ツールで自分で探します。カードにtransformを入れるとオーバーレイがどう閉じ込められるか、祖先のoverflow: hiddenがstickyをどう無効にするかを、目で確認します。