プロンプトと適用されるコンテキスト
ある製品ページに、sticky ヘッダーとメニューボタンを含むカードがあります。メニューは絶対配置(position: absolute)され、非常に大きなスタッキング値が与えられています。ページ上部付近でメニューを開くと、ヘッダーが依然としてその上に描画されます。また、メニューがカードの外に広がると、その下部が切り取られて見えなくなります。チームメイトはスタッキング値にゼロをもう1つ追加することを提案しています。
その提案では両方の問題を解決できない理由を説明してください。以下の例を診断し、描画順序(paint order)をクリッピングや配置の幾何構造(geometry)と区別した上で、3つのケース(コンポーネント内に留めることができるメニュー、コンポーネントから脱出する必要があるオーバーレイ、モーダルダイアログまたはポップオーバーとしてのセマンティクスが必要なUI)に対する持続可能な修正策を選択してください。
<header class="site-header">Navigation</header>
<main class="page">
<section class="card">
<button type="button">Open menu</button>
<div class="menu">Menu items</div>
</section>
</main>.site-header {
position: sticky;
top: 0;
z-index: 4;
}
.page {
position: relative;
z-index: 1;
}
.card {
position: relative;
overflow: hidden;
transform: translateZ(0);
}
.menu {
position: absolute;
inset-block-start: 100%;
z-index: 999999;
}現在のモダンブラウザ(evergreen browsers)を想定してください。回答では、スクリーンショットの見た目が合うまでCSSを削除するのではなく、意図的なスクロール、コンテインメント、アクセシビリティの挙動を維持する必要があります。
面接官が評価するポイント
第1の評価シグナルは、候補者が比較の境界(境界単位)を理解しているかどうかです。スタッキングコンテキストは、親コンテキスト内において1つの分割できない単位(アトミックな単位)として描画されます。子孫要素はその単位の内部で順序を並べ替えることができますが、どの子孫のスタッキング値であっても、その単位全体を親の兄弟要素の上に引き上げることはできません。この例では、メニューの大きな値は .page の内部で優先されますが、.page 全体としてはレベル 1 のまま関与し、レベル 4 のヘッダーよりも下になります。
第2のシグナルは分類力です。「ヘッダーの背面に隠れる」は描画順序の問題です。「カードの端で切り取られる」は overflow: hidden によるクリッピングの問題です。また、似たように見える第3の分類として、transform を持つ祖先要素が絶対配置および固定配置の子孫に対して包含ブロック(containing block)を形成するため、座標や固定動作が意図しない祖先を基準にしてしまう問題があります。単一のスタッキング値を変更しても、クリッピングや幾何構造は修復できません。
第3のシグナルは、単なる丸暗記のリストではなく、コンテキストを生成する要因を理解しているかです。一般的な要因には、auto 以外のスタッキング値を持つ配置要素、fixed または sticky の配置、1未満の opacity、transform、filter、perspective、clip-path や mask、mix-blend-mode、isolation: isolate、layout または paint の containment、container queries、auto 以外のスタッキング値を持つ flex または grid の子要素が含まれます。有用なスキルは、各祖先チェーン上で最初の関連する境界を見つけ出すことです。
第4のシグナルは修正の判断力です。意図しないコンテキストの削除、正しい祖先のレベルの変更、オーバーレイのリペアレンティング(親の付け替え)、ブラウザのトップレイヤーの使用は、それぞれ異なる要件を解決します。ポータルはDOMの階層関係を変更してコンポーネントのクリッピングから脱出できますが、自動的にトップレイヤーに入ったり、モーダル動作、フォーカス制御、閉じる挙動、アクセシブルな名前の挙動を追加したりするわけではありません。
最後のシグナルは検証です。DevTools による検証は、重複部分の目視確認、スクロール、リサイズおよびズームの確認、キーボード操作、他のオーバーレイとの共存確認によって裏付けられる必要があります。1つのスクリーンショットで正しく見えても、スクロールコンテナを壊したり、誤った座標系にアンカーされたり、通常のメニューが本物のモーダルを覆ってしまったりするなら、その修正は不完全です。
回答前に確認すべき明確化のための質問
- どのピクセルが正しくないか? メニューが別の要素の下に描画されているのか、境界でクリップされているのか、誤った座標に配置されているのか、それともトップレイヤーの要素に覆われているのか? これらはそれぞれ異なる修正が必要です。
- どの祖先がコンテキストを生成しているか? メニューのチェーンと、それを覆っている要素のチェーンの両方を検査します。決定的な比較は、両要素そのものではなく、チェーン同士が最初に兄弟となる場所で行われます。
- クリッピングは意図的なものか? カードは角丸のメディアをクリップしている可能性があり、スクロールコンテナはオーバーフローの挙動を必要としている場合があります。これをグローバルに削除すると、メニューは修正されてもレイアウトやスクロールが壊れる可能性があります。
- オーバーレイは何を基準にアンカーされるべきか? 共通のオーバーレイ用ルートにリペアレントする場合、その座標はそのルートを基準に測定し、スクロール、リサイズ、ズーム、レイアウト変更時に更新する必要があります。
- UI にトップレイヤーのセマンティクスが必要か? モーダルダイアログは外部とのインタラクションをブロックし、フォーカスを管理する必要があります。軽量なメニューにはオーバーレイルートだけで十分な場合があります。適切なポップオーバーであれば、プラットフォームの Popover API とトップレイヤーの配置を使用できます。
- どのオーバーレイが共存し得るか? メニュー、ツールチップ、sticky ナビゲーション、ドロワー、モーダルが互いに重なり合えるかどうかを定義します。任意の6桁の数値よりも、少数の名前付きレイヤー(named layers)のセットの方が管理が容易です。
- どのようなブラウザおよびコンポーネントの要件が適用されるか? 最新のプラットフォームプリミティブが好まれる場合がありますが、古い WebView や既存のデザインシステムのプリミティブによって実装の選択肢が変わる場合があります。
30秒の回答フレームワーク
「値を変更する前に障害を分類します。メニューの 999999 は自身のスタッキングコンテキスト内でのみ比較されます。その祖先である .page はレベル 1 であるため、サブツリー全体がレベル 4 の sticky ヘッダーの下に残ります。それとは別に、カードの hidden なオーバーフローによってメニューがクリップされており、カードの transform によってメニューの包含ブロックが変わる可能性があります。祖先チェーンと、重なり合うポイントで実際に最上位にある要素を検査します。メニューをローカル内に留められる場合は、意図しないコンテキストを削除するか、正しい祖先のレベルを引き上げてクリッピングをローカルで対処します。コンポーネント外に脱出する必要がある場合は、共通のオーバーレイルートで描画し、アンカーの幾何構造を再計算します。操作がモーダルまたは適切なポップオーバーである場合は、ブラウザのトップレイヤーを使用し、必要なフォーカスと閉じる挙動の規約を実装します。その後、スクロール、リサイズ、ズーム、キーボード操作、他のオーバーレイとの共存をテストします。」
ステップバイステップの詳細解説
まず、4つのメカニズムを分離することから始めます。これらは同時に破綻することがありますが、交換可能ではありません。
PAINT ORDER
Which painted box is above another?
Controlled by the stacking-context tree and paint order inside each context.
CLIPPING
Which pixels are allowed to appear?
Controlled by overflow, clip paths, masks, and paint containment.
GEOMETRY
Which box supplies the coordinate system?
A transform can establish the containing block for absolute and fixed descendants.
TOP LAYER
Is the element outside normal document stacking?
Modal dialogs and shown popovers can be placed in the browser-managed top layer.2つの目に見える数値を比較するのではなく、この例のコンテキストツリーを構築します。sticky ヘッダーはレベル 4 でコンテキストを生成します。配置された .page は、レベル 1 で兄弟コンテキストを生成します。カードの transform は .page 内に別のコンテキストを生成します。メニューの大きな値は、そのサブツリーの内部でのみ順序を決めます。概念的には、比較は header: 4 対 page: 1 であり、決して header: 4 対 menu: 999999 ではありません。
次に、クリッピングを個別に検査します。メニューは .card の子孫であるため、カードのパディングボックスの外側にあるピクセルは hidden オーバーフローによってクリップされます。スタッキングは描画順序を変更するものであり、クリッピング領域を拡大するものではありません。.page を引き上げたり、transform を削除したり、メニューの値を変更したりしても、ヘッダーより上に表示されるようにはなりますが、下部は切り取られたままになります。
続いて、包含ブロックを検査します。メニューは absolute であるため、配置されたカードがすでにその座標を提供しています。この特定のメニューに対して transform は冗長ですが、将来の実装でメニューが position: fixed に切り替えられた場合に重要になります。transform が設定された祖先要素は、その「fixed」要素をビューポートではなくその祖先を基準にして動作させる可能性があります。このため、absolute を fixed に変更することは信頼できる解決策にはなりません。
推測に頼らず、ブラウザの検証機能を使って仮説を確認します。最初のコンテキスト生成要素とクリッピング祖先が見つかるまで、すべての祖先の計算済みスタイル(computed styles)を検査します。メニューとヘッダーのコンテキストチェーンを比較します。それらが重なる座標で、ヒットテストが何を最上位とみなしているかを問い合わせます。
function inspectOverlap(x, y) {
const stack = document.elementsFromPoint(x, y)
return stack.map((element) => ({
element,
position: getComputedStyle(element).position,
zIndex: getComputedStyle(element).zIndex,
overflow: getComputedStyle(element).overflow,
transform: getComputedStyle(element).transform,
}))
}elementsFromPoint() はヒットテストのスタックを確認するのに便利です。elementFromPoint() は最上位の対象要素のみを返します。ヒットテストは補助的な証拠であり、スタッキングコンテキストの完全なデバッガーではありません。pointer-events: none を持つ要素はスキップされる可能性があり、クリッピングや画面外の幾何構造は依然としてレイアウト検査が必要です。
所有権の境界(責任範囲)に合致する最小限の修正を選択します。
ローカルメニューの場合、視覚的効果やコンテインメント効果が必要なければ、意図しないコンテキストを削除します。ページが真にコンテキストを必要としている場合は、ヘッダーに対して順序付けられている兄弟要素であるため、ページレベルのトークンを比較して変更します。カードのクリッピングがメディアのみを対象としている場合は、インタラクティブなカード全体ではなく、ネストされたメディアラッパーにそのクリッピングを配置します。
:root {
--layer-content: 0;
--layer-sticky: 20;
--layer-overlay: 40;
}
.page {
position: relative;
z-index: var(--layer-content);
}
.site-header {
z-index: var(--layer-sticky);
}
.card-media {
overflow: hidden;
border-radius: 1rem;
}
.menu {
z-index: var(--layer-overlay);
}これは、意図された要件としてページのサブツリーがヘッダーの下に留まることが許容され、メニューがその境界を越える必要がない場合にのみ機能します。メニューがヘッダーを覆う必要がある場合、レイヤーの決定は共通の祖先で行うか、オーバーレイを共有ルートに移動する必要があります。
ローカルコンテキストとクリッピングから脱出する必要がある再利用可能なオーバーレイの場合、ドキュメントルート付近の共有オーバーレイルート配下に描画します。トリガーを論理的な所有者として維持し、アクセシブルな関係性を保ち、トリガーの矩形からパネルの位置を計算します。スクロール、リサイズ、ズーム、またはレイアウト変更によってどちらかの矩形が変化した場合は再計算します。衝突処理(collision handling)ではビューポートの端と書字方向を考慮する必要があります。幾何構造の管理なしでリペアレントを行うと、クリッピングのバグが「メニューの位置ずれバグ」に置き換わるだけになります。
本物のモーダルダイアログの場合、モーダルとして開かれたネイティブの dialog 要素、または同じ挙動を提供する確立されたアクセシブルなプリミティブを使用します。適切な非モーダルインタラクションの場合、Popover API により表示コンテンツをトップレイヤーに配置できます。トップレイヤーのエントリは通常のドキュメントコンテキストの上に描画され、後から追加されたエントリは以前のエントリの上に配置され、それぞれが独自のルートコンテキストを持ちます。祖先要素の overflow、opacity、mask、transform によってトップレイヤーの要素が祖先のドキュメントコンテキストに引き戻されることはありません。
トップレイヤーは、万能な「より強力な z-index」ではありません。セマンティックな出入りメカニズムを備えた、ブラウザが管理する順序付けられたレイヤーです。モーダルには、適切な名前、意図的な初期フォーカス、閉じ込められたインタラクション、閉じるポリシー、フォーカスの復元が依然として必要です。メニューには、依然として適切なメニューまたはディスクロージャーのセマンティクスとキーボード動作が必要です。レイヤーの選択は、インタラクションモデルの設計の代わりにはなりません。
修正内容をマトリクスとして検証します。
- 各ビューポートの端付近や、ネストされたスクロールコンテナの内部で開く。
- ページと最も近いコンテナをスクロールし、パネルがアンカーされたまま留まるか、設計通りに閉じるかを検証する。
- リサイズし、200% にズームし、狭いレイアウトと広いレイアウトをテストする。
- キーボードでナビゲートし、順序、可視フォーカス、Escape キー、フォーカスの復帰がコンポーネントの仕様と一致していることを検証する。
- sticky ナビゲーション、ドロワー、ツールチップ、本物のモーダルが既に存在している状態で開く。
- 隠れたピクセルでのヒットテストを確認し、不可視のオーバーレイが無関係なコントロールを遮断していないことを確認する。
- 翻訳による長いラベルや、サポートされている場合は LTR(左横書き)と RTL(右横書き)の両方のレイアウトをテストする。
- 意図的な overflow、containment、合成(compositing)、スクロールの挙動が引き続き機能していることを確認する。
質の高い模範解答
「大きな値が誤ったレベルで比較されています。メニューは .page のスタッキングコンテキスト内にあり、これはレベル 1 で関与しています。ヘッダーはレベル 4 の兄弟コンテキストです。ブラウザはページのサブツリーをヘッダーの下にアトミックに描画するため、子孫の 999999 という値が外に出てヘッダーと競合することはできません。
また、それとは別にクリッピングの障害もあります。カードの hidden オーバーフローはその境界の外側にあるピクセルを削除します。z-index は順序を変更しますが、そのクリップを拡大することはできません。カードの transform は別のコンテキストを作成し、absolute や fixed の子孫に対して包含ブロックを形成する可能性があるため、fixed 配置に切り替えても予期しない座標系が使用される可能性があります。
DevTools で両方の祖先チェーンを検査し、最初のコンテキスト生成要因とクリッピング生成要因を特定し、重なり合う座標でのヒットテストスタックを確認します。ローカルメニューの場合、意図しないコンテキストのみを削除するか、クリッピングをメディアラッパーに移動するか、実際に比較されている共通の祖先の名前付きレイヤーを調整します。末端の値を増やし続けることはしません。
メニューが仕様上コンポーネントやヘッダーの境界を越えることが許可されている場合は、共有オーバーレイルートで描画します。これは、オーバーレイシステムが測定、衝突処理、スクロール・リサイズ・ズーム・レイアウト変更時の更新を担うことを意味します。ポータルは階層関係とクリッピングには役立ちますが、トップレイヤーやアクセシビリティの挙動を作成するわけではありません。
インタラクションがモーダルダイアログである場合は、ネイティブのモーダルダイアログ動作または検証済みのアクセシブルなプリミティブを使用します。適切なポップオーバーである場合は、プラットフォームの Popover 動作を検討します。どちらもブラウザのトップレイヤーを使用できますが、アクセシブルな名前、フォーカス、キーボード、閉じる動作、復元の規約を正しく実装する必要があります。最後に、スクロールコンテナ、ビューポートの端、200% ズーム、キーボード操作、翻訳されたレイアウト、ヒットテスト、他のオーバーレイとの共存を検証します。」
よくある間違い
- 子孫の値を際限なく増やす → 下位の親コンテキストに閉じ込められたままになる → 最初の兄弟コンテキストを比較し、そのレイヤー決定の所有者を変更する。
- 非表示のピクセルをすべてスタッキングのバグとして扱う → overflow や paint の containment によって依然としてクリップされる → 描画順序とは別にクリッピングの祖先を検査する。
- absolute 配置を fixed に変更する → transform された祖先が依然として包含ブロックを定義している可能性がある → 配置モードを選択する前に座標系を追跡する。
- すべての transform や overflow の宣言を削除する → 角丸メディア、スクロール、コンテインメント、またはレンダリングの挙動が壊れる → 意図しないトリガーのみを削除するか、それを必要とする要素のみに絞り込む。
- すべてのオーバーレイを body にポータルする → アンカー、衝突、所有権、フォーカスの問題が元のバグに取って代わる → 明示的な幾何構造とインタラクションの規約を持つ共有オーバーレイシステムを使用する。
- ポータルをトップレイヤーであると思い込む → 通常のドキュメントコンテキストが依然としてそれを覆う可能性がある → セマンティクスが合致する場合はプラットフォームのトップレイヤーのプリミティブを使用する。
- すべてのフローティングパネルにモーダルダイアログを使用する → フォーカスや外部とのインタラクションが不必要にブロックされる → まず挙動からモーダル、ポップオーバー、メニュー、ツールチップ、インラインディスクロージャーを選択する。
- 任意のグローバル値を作成する → コンポーネントがエスカレートするレイヤリング競争に突入する → 少数の名前付きレイヤーのセットを使用し、ローカルな値はローカルに留める。
- 1つのスクリーンショットのみを検証する → スクロール、ズーム、キーボード、または別のオーバーレイによって不具合が再発する → 重なりとインタラクションのマトリクスをテストする。
フォローアップの質問と回答
大きな z-index は常に小さな z-index の要素の上に配置されますか?
それらの値が同じスタッキングコンテキストの比較に関与している場合にのみ成り立ちます。子孫はそのコンテキスト内で順序付けられ、そのコンテキスト全体が親の中で順序付けられます。孤立した計算値ではなく、コンテキストチェーンを比較してください。
スタッキングコンテキストを生成する一般的な CSS プロパティは何ですか?
ルート要素、fixed または sticky の配置、auto 以外のスタッキング値を持つ配置要素、1未満の opacity、transform、filter、perspective、clip-path や mask、mix-blend-mode、isolation、一部の containment や container-query プロパティ、auto 以外のスタッキング値を持つ flex または grid の子要素が一般的な要因です。正確なリストは変化するため、記憶だけに頼るのではなく、検査によって計算されたトリガーを特定する必要があります。
要素が兄弟要素より上にあるにもかかわらず、切り取られて見えるのはなぜですか?
描画順序は、許可されたどのピクセルが最後に描画されるかを決定します。クリッピング領域は、どのピクセルの存在が許可されるかを決定します。描画順序で勝っても、祖先の overflow、clip-path、mask、または paint containment によって削除されたピクセルを復元することはできません。
position: fixed は常に overflow: hidden の祖先から脱出できますか?
いいえ。fixed 配置は通常の包含ブロックの挙動を変更しますが、祖先の transform や関連プロパティが包含ブロックを形成する場合があります。また、他のクリッピングメカニズムが適用されることもあります。fixed 配置を万能な解決策として使うのではなく、幾何構造とクリッピングを追跡してください。
ポータルを介して描画すれば問題はすべて解決しますか?
ターゲットのルートが外部にあれば、ローカルの DOM 階層関係、コンテキスト、クリッピングから脱出できます。ただし、測定、スクロール更新、衝突処理、イベント関係、フォーカス、閉じる処理の新たな管理責任が発生します。プリミティブ自体がトップレイヤーに入らない限り、通常のドキュメントスタッキング内に留まります。
トップレイヤーは、非常に高いグローバルレイヤートークンとどう異なりますか?
トップレイヤーの要素は、通常のドキュメントコンテンツの上のブラウザが管理する順序付きレイヤーに描画され、ルートスタッキングコンテキストを作成します。エントリの順序によってそのレイヤー内の順序が決まります。高いグローバルトークンは依然としてドキュメントのコンテキストツリーに関与しており、祖先によって閉じ込められる可能性があります。
チームはすべてのローカルな z-index 値を禁止すべきですか?
いいえ。ローカルな値は、画像の上にバッジを配置するなど、1つのコンポーネント内のパーツを順序付けるのに有用です。sticky ナビゲーション、再利用可能なオーバーレイ、ドロワーなどの共有境界でのみ、少数の名前付きスケールを使用してください。数値の大きさではなく、その境界が値をグローバルにするかどうかを決定します。
この種のリグレッションをどのように防止しますか?
少数の共有レイヤー所有者を文書化し、再利用可能なオーバーレイの配置を一元化し、sticky コンテンツ、スクロールコンテナ、メニュー、モーダルを組み合わせたインタラクションストーリーを追加します。端の配置、スクロール、ズーム、キーボード操作、別のオーバーレイのアクティブ化を検証します。静的なスナップショットだけでは要件が満たされていることを証明できません。