包含ブロックと位置指定(position)の解決過程
absoluteが思わぬ親に吸着する、stickyが効かない、transformの中でfixedが固定されない——その全てを包含ブロックの決定規則から原理で説明し、当て推量なしに直せるようになります。
- 1.包含ブロックは position で決まる。static/relative は直近ブロック祖先のコンテンツ領域、absolute は直近の「位置指定された」祖先のパディング領域、fixed はビューポートが基準になる。
- 2.祖先に transform/filter/will-change/contain などがあると、その要素が fixed/absolute の包含ブロックを新規生成する。これが「fixed が画面に固定されない」典型原因。
- 3.sticky は包含ブロック内で relative と fixed を切り替える挙動で、最近接スクロールポート基準のオフセット閾値と、はみ出し不可の親矩形による上限の2条件で位置が決まる。
包含ブロックがすべての基準を握る
要素の位置やパーセンテージ寸法は、単独では決まりません。必ず**包含ブロック(containing block)**という基準矩形が先にあり、top/left などのオフセットや width: 50% はその矩形に対して解決されます。つまり「absolute がどこに吸い付くか」「fixed が本当に画面に固定されるか」「width: 100% が何の100%か」は、すべて包含ブロックの決まり方ひとつで説明できます。逆に言えば、位置指定のバグの大半は「想定と違う矩形が包含ブロックになっている」ことが原因です。
包含ブロックは要素の position の値によって、まったく異なるルールで決定されます。まずはこの対応表が出発点です。
| position | 包含ブロックの決まり方 | 基準にする領域 |
|---|---|---|
| static / relative | 直近のブロックコンテナ祖先 | コンテンツ領域 |
| absolute | 直近の「位置指定された」祖先 | パディング領域 |
| fixed | ビューポート(原則) | ビューポート矩形 |
| sticky | 直近のスクロールポート+ブロック祖先 | 両方の合わせ技(後述) |
ここで「位置指定された祖先(positioned ancestor)」とは、position が static 以外(relative/absolute/fixed/sticky)の祖先を指します。absolute 要素は親をさかのぼり、最初に見つかった positioned 祖先のパディング領域を基準にします。途中の祖先がすべて static なら、最終的に**初期包含ブロック(initial containing block、おおよそビューポート)**にたどり着きます。これが「position: relative を付け忘れた親を absolute の子が突き抜けて、ページ全体の左上に飛ぶ」現象の正体です。
absolute の基準が positioned 祖先の「パディング領域」である点はよく混同されます。top: 0; left: 0 の子は祖先のボーダーの内側(パディングの外縁)に揃い、ボーダー幅ぶん内側に入ります。一方 static/relative の包含ブロックは祖先のコンテンツ領域(パディングの内側)です。基準にする矩形が position によって border・padding・content と一段ずつ違うことを意識すると、1px のズレに悩まなくなります。
パーセンテージ解決の非対称性
包含ブロックは「位置」だけでなく「サイズのパーセンテージ」の基準でもあります。ここに方向ごとの非対称があります。width: 50% は包含ブロックの幅、height: 50% は包含ブロックの高さを基準にします。問題は高さ側で、包含ブロックの高さが auto(自動)だと、縦方向のパーセンテージは解決できず無視される点です。これが「親に高さ指定がないと子の height: 100% が効かない」の原理です。
さらに padding と margin は、縦横どちらも一律で包含ブロックの幅を基準にします。padding-top: 50% が親の高さでなく幅基準なのはこのためで、正方形を作る古典的アスペクト比ハックの根拠でした(現在は aspect-ratio で代替可能)。
width: 50% → 包含ブロックの「幅」基準
height: 50% → 包含ブロックの「高さ」基準(高さが auto なら無視)
padding/margin の % → 縦横とも包含ブロックの「幅」基準
transform/filter が包含ブロックを生む例外
最も事故が多いのが、fixed(および absolute)の包含ブロックを通常はあり得ない要素が奪うケースです。原則として fixed の基準はビューポートですが、祖先に次のいずれかが効いていると、その祖先が新たな包含ブロックを生成し、fixed 子はビューポートではなくその祖先を基準にしてしまいます。
| プロパティ | 条件 | 影響を受ける position |
|---|---|---|
| transform | none 以外の値 | fixed と absolute |
| filter / backdrop-filter | none 以外の値 | fixed(実装により absolute も) |
| perspective | none 以外の値 | fixed と absolute |
| will-change | transform / filter / perspective を指定 | fixed と absolute |
| contain | layout / paint / strict / content | fixed と absolute |
仕組みは一貫しています。transform や filter は要素に独立した座標系・合成レイヤーを与えるため、その内側の子は「画面」ではなく「この変換された箱」を基準にしなければ整合が取れません。結果として、transform: translateZ(0) を高速化目的で付けた親の内側にある position: fixed のモーダルが、スクロールに追従して動いてしまう——という典型バグが生まれます。will-change も同じ理由で、ブラウザが事前にレイヤーを用意するため包含ブロックを生成します。
position: fixed が画面に張り付かない場合、まず祖先をさかのぼって transform/filter/perspective/will-change/contain のいずれかが効いていないかを確認します。アニメーション用に何気なく付けた transform、backdrop-filter のすりガラス、パフォーマンス目的の will-change が犯人であることがほとんどです。解決策は、その祖先の外側へ fixed 要素を移すか、当該プロパティを外すかの二択になります。
sticky の計算過程
position: sticky は relative と fixed の中間的な振る舞いをします。要素は通常フロー上の位置(relative と同じ)を保ちつつ、最近接のスクロールポートに対して指定した閾値(top/bottom/left/right)に達すると、その閾値の位置で**貼り付き(stuck)**ます。重要なのは、sticky には2つの異なる基準が同時に関わることです。
- 粘着の閾値:最近接の**スクロールコンテナ(スクロールポート)**を基準に、
top: 0なら「ビューポート上端から0px」で貼り付き始める。 - 可動域の上限:sticky 要素は自分の包含ブロック(=ブロックレベルの親要素の矩形)からはみ出せない。親矩形の下端まで来たら、それ以上は貼り付かず親と一緒にスクロールアウトする。
この2条件から、各フレームでの位置は次のように解けます。
1. 通常フロー上の本来位置 flow_pos を求める
2. スクロール量から「閾値に貼り付くべき位置」stuck_pos を求める
(例 top:10px なら スクロールポート上端 + 10px)
3. 親(包含ブロック)の上端・下端で許される範囲 [min, max] にクランプ
4. 最終位置 = clamp( max(flow_pos, stuck_pos), min, max )
つまり sticky は「relative の本来位置」と「fixed 的な貼り付き位置」のうちより進んだ方を採用し、さらに親矩形の範囲でクランプします。ここから「効かない sticky」の二大原因が導けます。
1つ目は、祖先のどこかに overflow: hidden/auto/scroll/clip が付いているケース。これがあると、その要素が新しいスクロールポートになり、sticky はそのポート内でしか粘着しません。ページ全体に対して貼り付かせたいのに、すぐ近くの小さなクリップ祖先を基準にしてしまい、見かけ上「効かない」状態になります。2つ目は、親(包含ブロック)の高さが sticky 要素とほぼ同じケース。可動域がゼロなので貼り付く前にクランプされ、ただスクロールアウトします。sticky の親には十分なスクロール余地が必要です。
解決の全体フロー
position 別の包含ブロック決定を、判定の流れとしてまとめると次のようになります。
position == static / relative
→ 直近のブロックコンテナ祖先のコンテンツ領域
position == absolute
→ 祖先を上へ探索
transform/filter/contain 等を持つ祖先があれば、それ
なければ直近の positioned 祖先のパディング領域
それも無ければ初期包含ブロック(≒ビューポート)
position == fixed
→ 祖先に transform/filter/perspective/will-change/contain
を持つ要素があれば、そのパディング領域
なければビューポート
position == sticky
→ スクロール閾値は最近接スクロールポート基準
可動域は直近ブロック祖先の矩形でクランプ
頻出は、(1) absolute の基準は「直近 positioned 祖先のパディング領域」、static/relative は「コンテンツ領域」という領域の違い、(2) fixed の基準はビューポートだが transform/filter/will-change/contain を持つ祖先があればそちらに奪われる例外、(3) 縦パーセンテージは包含ブロック高さが auto だと無視され、padding/margin の % は常に幅基準という非対称、(4) sticky は「閾値(スクロールポート基準)」と「親矩形によるクランプ」の2条件で位置が決まり、overflow を持つ祖先や可動域不足で効かなくなる点、の4つです。
まとめ
位置指定は包含ブロックという基準矩形の決定がすべての出発点です。static/relative は直近ブロック祖先のコンテンツ領域、absolute は直近 positioned 祖先のパディング領域、fixed はビューポートを基準にします。ただし transform/filter/perspective/will-change/contain を持つ祖先は独立した座標系を作るため、fixed/absolute の包含ブロックを横取りします——これが「fixed が固定されない」最大の原因です。パーセンテージは方向で基準が分かれ、縦は包含ブロック高さが auto だと無視、padding/margin の % は常に幅基準。sticky は「最近接スクロールポート基準の閾値」と「親矩形によるクランプ」の2条件で位置を解き、overflow を持つ祖先や可動域不足で効かなくなります。サイズ側の解決規則は CSSレイアウトアルゴリズム(Flexbox/Gridの解決過程)、土台のボックスモデルは CSS、transform/filter が生むレイヤーの性能影響は リフローとリペイントを引き起こすCSSプロパティの分類 と レンダリングパイプライン詳説 で押さえると、位置の解決から合成までが一本でつながります。
Web/フロントエンド Article
包含ブロックと位置指定(position)の解決過程を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
CSS
比較で見る軸
難易度: advanced / カテゴリ: Web/フロントエンド / タグ数: 5
導入後に効く点
祖先に transform/filter/will-change/contain などがあると、その要素が fixed/absolute の包含ブロックを新規生成する。これが「fixed が画面に固定されない」典型原因。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- Web/フロントエンド
- タグ数
- 5
判断チェックリスト
- 自社の用途が「CSS / レイアウト」に近いか確認する。
- 強みである「包含ブロックは position で決まる。static/relative は直近ブロック祖先のコンテンツ領域、absolute は直近の「位置指定された」祖先のパディング領域、fixed はビューポートが基準になる。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。