題幹與適用場景
一個產品頁面包含置頂導覽與帶有選單按鈕的卡片。選單採用絕對定位,並設定了非常大的層疊值。 它在頁面頂端附近展開時,導覽仍會蓋在選單上方;它超出卡片邊界時,下半部又會消失。有同事 建議在層疊值後面再加一個零。
請解釋這個方案為何無法同時解決兩個現象。診斷下列範例,區分繪製順序、裁切與定位座標,並 針對三種情況選擇可長期維護的修復:選單可以留在元件內部、浮層必須脫離元件邊界,以及互動 語意適合模態視窗或彈出層。
<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;
}假設目標環境是目前主流瀏覽器。答案應保留刻意設計的捲動、包含與無障礙行為,不能為了讓一張 截圖看起來正常就不斷刪除 CSS。
面試官考察點
第一項訊號是候選人能否找到正確的比較邊界。一個層疊上下文會作為原子整體參與父層上下文的 繪製。後代可以在這個整體內部重新排序,但後代的任何層疊值都不能把整個父層上下文抬到其兄弟 之上。在本題中,選單的超大值只在 .page 內部勝出;.page 整體仍以層級 1 參與比較, 低於層級 4 的導覽。
第二項訊號是問題分類。「被導覽蓋住」屬於繪製順序問題;「在卡片邊緣消失」屬於 overflow: hidden 造成的裁切問題。還有一類外觀相似的問題:帶有變形的祖先會為絕對定位與 固定定位的後代建立包含區塊,導致座標或固定行為相對了錯誤的祖先。一個層疊值無法修復裁切與座標。
第三項訊號是理解層疊上下文的建立原因,而不是背誦一張清單。常見原因包括:定位元素設定非 auto 層疊值、固定或黏性定位、小於一的不透明度、變形、濾鏡、透視、裁切或遮罩、混合模式、 isolation: isolate、版面或繪製包含、容器查詢,以及設定非 auto 層疊值的 flex 或 grid 子項。真正有用的能力是沿著兩條祖先鏈找到第一個相關邊界。
第四項訊號是修復判斷。移除意外建立的上下文、調整正確祖先的層級、重新掛載浮層與使用瀏覽器 頂層,解決的是不同契約。Portal 能改變 DOM 祖先關係並避開元件裁切,但不會自動進入頂層,也 不會自動提供模態語意、焦點、關閉或無障礙名稱行為。
最後一項訊號是驗證。除了在開發者工具中檢查,還應透過真實重疊點、捲動、尺寸變化、縮放、 鍵盤與其他浮層共存來驗證。如果修復只贏了一張截圖,卻破壞捲動容器、綁定到錯誤座標系,或 讓一般選單蓋住真正的模態視窗,答案仍不完整。
回答前需要釐清的問題
- 具體哪些像素不對? 選單是繪製在另一個元素下方、在某個邊界被裁切、座標錯誤,還是被
頂層元素覆蓋?這些問題需要不同修復。
- 哪些祖先建立了上下文? 同時檢查選單與遮擋元素的祖先鏈。真正的比較發生在兩條鏈首次
成為兄弟的位置,不一定發生在兩個可見元素本身。
- 裁切是否刻意為之? 卡片可能需要裁切圓角媒體,捲動容器也可能依賴 overflow。全域移除
它可能修好選單,卻破壞版面與捲動。
- 浮層應錨定在哪裡? 重新掛載到共享浮層根節點後,需要相對該根節點測量座標,並在捲動、
調整視窗、縮放與版面變化時更新。
- 互動是否需要頂層語意? 模態視窗應阻止外部互動並管理焦點;輕量選單可能只需共享浮層根;
合適的彈出內容可以使用平台的 popover 行為與頂層位置。
- 哪些浮層可以共存? 明確選單、提示、置頂導覽、抽屜與模態視窗能否相互覆蓋。少量命名層級
比任意六位數更容易推理。
- 瀏覽器與元件契約是什麼? 目前平台原語通常更合適;舊版 WebView 或既有設計系統原語可能
改變實作選擇。
30 秒回答框架
「我會先替故障分類,再改數值。選單的 999999 只在自己的層疊上下文內比較。它的祖先 .page 是層級 1,所以整個子樹仍低於層級 4 的置頂導覽。卡片的隱藏溢位又會單獨裁切 選單,而變形可能改變選單的包含區塊。我會檢查兩條祖先鏈,並確認重疊點實際位於最上方的元素。 如果選單可以留在局部,就移除意外上下文或調整正確祖先,並把裁切限制在需要它的子元素。如果 必須脫離元件,就渲染到共享浮層根節點並重新維護錨點座標。如果互動是模態視窗或合適的 popover, 就使用瀏覽器頂層並實作相應焦點與關閉契約。最後驗證捲動、尺寸變化、縮放、鍵盤與其他浮層共存。」
分步深入解答
先把四種機制分開。它們可能同時失敗,但不能互相替代:
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.為範例建立上下文樹,不能只比較兩個可見數值。置頂導覽建立層級 4 的上下文;定位的 .page 建立層級 1 的兄弟上下文;卡片的變形又在 .page 內建立一個上下文。選單的超大值只在這個 子樹內部排序。概念上,比較的是 header: 4 與 page: 1,瀏覽器不會直接比較 header: 4 與 menu: 999999。
接著獨立檢查裁切。選單是 .card 的後代,超出卡片內距框的像素會被隱藏溢位裁掉。層疊只 改變繪製順序,不會擴大裁切區域。抬高 .page、移除變形或改變選單數值,都可能讓它高於導覽, 但下半部仍然消失。
再檢查包含區塊。選單採用絕對定位,因此已定位的卡片本來就提供其座標。對目前選單而言,變形 是多餘的;但如果未來實作把選單改成 position: fixed,變形祖先可能讓這個「固定」元素相對 祖先而非視口定位。因此,把絕對定位改成固定定位不是可靠的脫離方式。
用瀏覽器檢查驗證推斷,避免猜測。檢查每個祖先的計算樣式,直到找到第一個上下文建立者與裁切 祖先;比較選單與導覽的上下文鏈;再在發生重疊的座標查詢命中測試認定的最上層元素:
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 的元素可能被略過, 裁切或位於畫面外的座標仍需檢查版面。
根據所有權邊界選擇影響最小的修復。
對於局部選單,如果某個上下文沒有必要的視覺或包含作用,就移除這個意外邊界。如果頁面確實 需要自己的上下文,應比較並調整頁面層級 token,因為參與導覽比較的是這個兄弟。如果卡片裁切 只用於媒體,把裁切下沉到媒體包裝層,不要讓整張互動卡片負責裁切。
: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);
}這個方案只適用於頁面子樹可以繼續位於導覽下方、選單不必跨越該邊界的契約。如果選單必須覆蓋 導覽,層級決策就屬於共同祖先,或者浮層需要移動到共享根節點。
對於必須脫離局部上下文與裁切的可重用浮層,把它渲染到接近文件根的共享浮層根節點。觸發器 仍是邏輯所有者,保留無障礙關聯,並根據觸發器矩形計算面板位置。捲動、視窗尺寸、縮放或版面 變化使任一矩形改變時都要重新計算;碰撞處理還要考慮視口邊緣與書寫方向。只重新掛載而不接管 座標,會把裁切問題換成選單漂移問題。
對於真正的模態視窗,優先使用以模態方式開啟的原生 dialog 元素,或提供同等契約的成熟無障礙 原語。對於合適的非模態互動,popover API 可以把已顯示的內容放入頂層。頂層項目繪製在普通 文件上下文之上,後進入的項目位於先進入的項目上方,每個項目都有自己的根層疊上下文。祖先的 overflow、不透明度、遮罩與變形不會把頂層元素重新拉回祖先的文件上下文。
頂層不是「更強的 z-index」。它是由瀏覽器管理、透過特定機制進入與離開的有序層。模態視窗 仍需有可用名稱、明確的初始焦點、受限的互動、關閉策略與焦點還原。選單仍需正確的選單或展開 語意及鍵盤行為。選擇層級不能取代選擇互動模型。
依照矩陣驗證修復:
- 在每個視口邊緣與巢狀捲動容器內開啟浮層。
- 分別捲動頁面與最近容器,確認面板持續錨定,或依產品策略關閉。
- 調整視窗、縮放至 200%,並測試窄螢幕與寬螢幕版面。
- 使用鍵盤導覽,確認順序、可見焦點、Escape 與焦點返回符合元件契約。
- 分別與置頂導覽、抽屜、提示及已開啟的真正模態視窗共存。
- 在被遮擋像素處檢查命中測試,確認沒有不可見浮層攔截無關控制項。
- 在產品支援時測試長翻譯文字、由左至右與由右至左版面。
- 確認原本刻意設計的 overflow、contain、合成與捲動行為仍正常。
高品質示範回答
「超大數值在錯誤層級參與比較。選單位於 .page 層疊上下文中,而這個上下文以層級 1 參與 父層比較;導覽是層級 4 的兄弟上下文。瀏覽器把頁面子樹作為整體繪製在導覽下方,所以後代的 999999 無法脫離並與導覽競爭。
這裡還有獨立的裁切故障。卡片的隱藏溢位會移除邊界外的像素;z-index 只改變順序,不能擴大 裁切區域。卡片變形還會建立另一個上下文,並可能為絕對定位與固定定位後代建立包含區塊,所以 改用固定定位仍可能落在意外座標系中。
我會在開發者工具中檢查兩條祖先鏈,找到第一個上下文與裁切建立者,並檢查重疊座標的命中測試 堆疊。對於局部選單,我只會移除意外上下文、把裁切下沉到媒體包裝層,或調整實際參與比較的 共同祖先上的命名層級,不會繼續增大葉節點數值。
如果選單的產品契約允許它跨越元件與導覽邊界,我會把它渲染到共享浮層根節點。這代表浮層系統 需要負責測量、碰撞處理,以及捲動、尺寸、縮放與版面變化時的更新。Portal 有助於改變祖先關係 並脫離裁切,但不會建立頂層或無障礙行為。
如果互動是模態視窗,我會使用原生模態 dialog 行為或經過驗證的無障礙原語;如果是合適的 popover,會考慮平台 popover 行為。兩者都可以使用瀏覽器頂層,但仍需實作正確的無障礙名稱、 焦點、鍵盤、關閉與還原契約。最後我會驗證捲動容器、視口邊緣、200% 縮放、鍵盤、翻譯版面、 命中測試以及與其他浮層共存。」
常見錯誤
- 不斷增大後代數值 → 它仍被困在較低的父層上下文 → **比較第一個兄弟上下文,並修改真正
擁有層級決策的節點。**
- 把所有消失像素都當作層疊問題 → overflow 或繪製包含仍然裁切 → **把裁切祖先與繪製順序
分開檢查。**
- 把絕對定位改成固定定位 → 變形祖先仍可能定義包含區塊 → 選擇定位模式前先追蹤座標系。
- 刪除所有 transform 或 overflow → 圓角媒體、捲動、包含或渲染行為遭到破壞 → **只移除
意外觸發器,或把它縮小到真正需要的元素。**
- 把每個浮層都 portal 到 body → 錨定、碰撞、所有權與焦點問題取代原故障 → **使用明確
管理座標與互動契約的共享浮層系統。**
- 認為 portal 就是頂層 → 普通文件上下文仍可能覆蓋它 → 在語意匹配時使用平台頂層原語。
- 所有浮動面板都使用模態視窗 → 焦點與外部互動遭到不必要的阻斷 → **先依行為選擇模態視窗、
popover、選單、提示或行內展開。**
- 建立任意全域大數值 → 元件開始層級軍備競賽 → 使用少量命名層級,並讓局部數值保持局部。
- 只驗證一張截圖 → 捲動、縮放、鍵盤或另一個浮層會重新暴露問題 → 測試完整重疊與互動矩陣。
追問與應對
更大的 z-index 一定會位於較小的值上方嗎?
只有兩個值參與同一個相關層疊上下文比較時才成立。後代先在自身上下文內排序,整個上下文再到 父層排序。應比較上下文鏈,不能只看孤立的計算值。
哪些 CSS 屬性經常建立層疊上下文?
根元素、固定或黏性定位、設定非 auto 層疊值的定位元素、小於一的不透明度、變形、濾鏡、透視、 裁切或遮罩、混合、隔離、部分包含與容器查詢屬性,以及設定非 auto 層疊值的 flex 或 grid 子項都很常見。具體清單會演進,因此排查時應找出實際計算樣式觸發器,不能只依賴記憶。
為什麼元素能蓋住兄弟,卻仍然像被切掉一樣?
繪製順序決定允許出現的像素由誰最後繪製;裁切區域決定哪些像素獲准出現。即使贏得繪製順序, 也無法找回被祖先 overflow、clip path、mask 或繪製包含移除的像素。
position: fixed 一定能脫離 overflow-hidden 祖先嗎?
不能。固定定位改變通常的包含區塊行為,但祖先的變形與相關屬性仍可能建立包含區塊,其他裁切 機制也可能繼續生效。應追蹤座標與裁切,不能把固定定位當成通用脫離方案。
透過 Portal 渲染能解決全部問題嗎?
如果目標根位於局部上下文與裁切之外,它可以脫離這些 DOM 祖先限制,但也引入新的測量、捲動 更新、碰撞、事件關聯、焦點與關閉所有權。除非原語本身進入頂層,否則它仍在普通文件層疊中。
頂層與一個很大的全域層級 token 有什麼差異?
頂層元素繪製在瀏覽器管理、位於普通文件內容上方的有序層中,並建立根層疊上下文;進入順序 決定頂層內部順序。很大的全域 token 仍參與文件上下文樹,也可能被祖先困住。
團隊是否應該禁止所有局部 z-index?
不應該。局部值適合排列同一元件內部的元素,例如讓徽章蓋在圖片上。只在置頂導覽、可重用浮層、 抽屜等共享邊界使用少量命名尺度。一個值是否全域取決於邊界,不取決於數字大小。
如何防止這類問題回歸?
記錄少量共享層級的所有者,集中管理可重用浮層位置,並建立同時包含置頂內容、捲動容器、選單與 模態視窗的互動場景。驗證邊緣位置、捲動、縮放、鍵盤與另一個活躍浮層;靜態截圖無法證明契約。