一行指向空氣的 grid-area,讓單欄版面多長出兩條軌道

引用一個不存在的具名區域,瀏覽器不會忽略,而是長出隱式軌道把版面算歪。附三個引擎的實測數據、截圖,以及 CSS Grid 規格上的三層成因。

手機寬度打開頁面,內容欄被壓成細細一條,中文幾個字就換一行。桌機版看起來沒什麼大問題。RWD 的 media query 明明寫著「小螢幕改單欄」,而且那段 CSS 是生效的。

這是這個網站在八月上線前真的遇過的 bug。專案的除錯紀錄(ARCHITECTURE.md 第 10.3 節)是這樣寫的:手機版整個被壓成一欄,中文逐字換行,單頁高度暴增到 5000px 左右,最後是用 Playwright 去量 computed style 才抓到。兇手是一行看起來完全無害的宣告:

.tag-sidebar {
  grid-area: sidebar;
}

父層的 .layout-grid 從來沒有定義過 grid-template-areas。也就是說,sidebar 這個區域根本不存在。

直覺上,引用一個不存在的具名區域應該跟拼錯屬性名一樣,瀏覽器看不懂就當沒看到。CSS Grid 的做法剛好相反:它會想辦法滿足你,方法是憑空多長幾條軌道出來。

為了寫這篇,我重新做了一份最小重現,在三個瀏覽器引擎上各跑一次。下面的數字跟截圖都是這次量的。

差一行,實際算出來差多少?

重現的設定很單純。兩欄 grid,父層 grid-template-columns: minmax(0, 1fr) 200px;@media (max-width: 1023px) 時改成單欄 1fr。唯一的變數是側邊欄有沒有那行 grid-area: sidebar。

先看手機寬度(390px)加上那一行之後的樣子:

390px 寬的重現頁面:加上 grid-area: sidebar 之後,文章內容欄被擠成 120px 寬的細條,側邊欄掉到右下角的另一列 圖:本文的最小重現在 390px 視窗下的實際畫面(Chromium,自行截圖)

內容欄只剩左邊一條,側邊欄被丟到右下角。注意它連「列」都換了,這點後面會解釋。

光看畫面你只知道壞了。要知道壞在哪,得去讀 getComputedStyle(layout).gridTemplateColumns,它回傳的是瀏覽器實際算完的軌道,而你寫進 CSS 的那串字它不管。

實測結果表:1280px 時軌道從 1080px 200px 變成 810px 200px 0px 270px;390px 時從單一 390px 變成 120px 0px 270px 圖:加上與不加 grid-area: sidebar 的軌道實測(本文量測,Chromium、Firefox、WebKit 三個引擎一致)

兩件事值得停下來看。

桌機版從 2 條軌道變成 4 條。你寫了兩條,瀏覽器給你四條,多出來的是一條 0px 跟一條 270px。那個 270px 是側邊欄內容自己撐出來的寬度。原本 CSS 指定的 200px 那條還在,只是裡面是空的。

手機版更違反直覺。CSS 白紙黑字寫 grid-template-columns: 1fr,算出來卻是 120px 0px 270px 三條。你去 DevTools 的 Styles 面板看,那行 1fr 好好地生效著,沒被劃掉也沒被覆寫。你的宣告沒有被蓋掉,是瀏覽器在它之外另外加了東西。

所以肉眼看只會覺得「排版怪怪的」,然後往字型、換行、flex 的 min-width 去查。欄數看起來不對,但你寫的欄數明明是對的。

這是瀏覽器的 bug 嗎?

我本來以為可能是。專案當初的紀錄只寫了 Chromium,所以這次我把 Firefox 跟 WebKit 也裝起來,同一份重現各跑一遍。

結果三個引擎算出來的軌道結構完全一樣:桌機都是四條,手機都是三條,數值只差在小數點後第三位的四捨五入(Chromium 給 810.281px,Firefox 給 810.283px,WebKit 給 810.28125px)。

三個各自獨立的實作「錯」成同一個樣子,通常代表它沒有錯,是規格要求這樣做。

桌機版的畫面長這樣,可以看到側邊欄不在它該在的那一欄,也不在第一列:

1280px 寬的重現頁面:內容欄只佔約 810px,側邊欄出現在右下方的第二列,原本 200px 的那一欄是空的 圖:同一份重現在 1280px 視窗下的畫面(Chromium,自行截圖)

規格到底規定了什麼?

這是 CSS Grid 規格裡三條規則接起來的結果。每一條單獨看都很合理。

第一層:一行簡寫展開成四個宣告

MDN 的 grid-area 頁面列出它是哪四個屬性的簡寫:

MDN 的 grid-area 頁面 Constituent properties 段落,列出 grid-row-start、grid-column-start、grid-row-end、grid-column-end 四個屬性 圖:MDN 列出 grid-area 所代表的四個屬性(來源:developer.mozilla.org)

只給一個名字的時候,四個屬性都拿到同一個值:

grid-area: sidebar;
/* 等同於 */
grid-row-start: sidebar;
grid-column-start: sidebar;
grid-row-end: sidebar;
grid-column-end: sidebar;

這裡就埋下伏筆了。一行簡寫變成橫向兩個、縱向兩個定位宣告。前面截圖裡側邊欄連「列」都跑掉,原因就在這:出事的不只是欄。

第二層:找不到具名線的時候

每個屬性會先去找叫 sidebar-start 或 sidebar-end 的具名線。grid-template-areas 會自動幫每個具名區域生出這組線。我們沒定義 grid-template-areas,所以找不到。

我去翻了 W3C 的 CSS Grid Level 1 規格,在 Line-based Placement 那一節,找不到的時候它是這樣處理的:

Otherwise, treat this as if the integer ‘1’ had been specified along with the <custom-ident>.

也就是退化成 grid-column-start: sidebar 1,意思是「第 1 條叫做 sidebar 的線」。

第三層:連叫這個名字的線都沒有

這是關鍵的一步。規格沒有叫瀏覽器放棄,它寫的是這段:

CSS Grid Level 1 規格原文:若以 custom-ident 指定名稱,只計算該名稱的線;若該名稱的線不夠,所有隱式格線都被假定擁有這個名稱 圖:CSS Grid Level 1 規格對「名稱不夠用」的處理方式(來源:w3.org)

「所有隱式格線都被假定擁有這個名稱。」瀏覽器為了讓 sidebar 1 有東西可以對應,就往顯式格線外面長出隱式軌道,把那邊的線當成你要的那條。

三層接起來:一個指向空氣的名字,被規格老老實實翻譯成「請在我定義的格線之外再開幾條軌道」。

那多出來的軌道為什麼是 0px 跟 270px?

這段是我對照量測結果的解讀,規格沒有逐字這樣寫。

隱式軌道的大小由 grid-auto-columns 決定,規格定義它的初始值是 auto。auto 軌道會依內容決定寬度,所以裝著側邊欄的那一條被內容撐到 270px,而它跟顯式格線之間空著的那一條因為沒有內容,就是 0px。

再往回推:原本的 1fr 是「吃掉剩下的空間」。現在有一條 270px 的軌道先佔了位置,390px 的視窗只剩 120px 給內容欄。內容欄就是這樣被擠扁的。

這也說明了為什麼每個網站壞掉的程度不一樣。側邊欄內容越寬,內容欄被擠得越窄。專案紀錄裡寫的是整欄被壓到接近逐字換行,我這份重現的側邊欄內容比較短,所以還留了 120px。

為什麼從頭到尾沒有任何警告?

我覺得這比 bug 本身更值得記住。

CSS 沒有「引用了不存在的東西」這種錯誤類別。屬性名拼錯,例如 grid-are: sidebar,會在解析階段被丟掉。但 grid-area: sidebar 的語法完全合法:<custom-ident> 是合法的值型別,sidebar 是合法的識別字,解析器沒有理由拒絕。這個名字指向什麼,要等版面演算法跑的時候才處理,而演算法的規格明確寫了「不夠就拿隱式線來湊」,所以那一步也不算失敗。

於是:

  • DevTools 的 Styles 面板不會標紅,因為那行宣告是有效的。
  • Build 不會失敗,對打包工具來說那只是一段合法的 CSS。
  • Console 是乾淨的。
  • Linter 這邊,要抓這個問題得知道另一個選擇器(甚至另一個檔案)有沒有定義對應的 grid-template-areas。這是跨選擇器的版面推理。我沒有把 stylelint 的規則清單逐條查過,所以不敢說一定沒有外掛做得到,但它不在一般 lint 的工作範圍裡。

整條工具鏈都說沒事,只有算出來的版面知道有事。這種安靜地做了別的事的失敗,比會噴錯的失敗難抓得多,因為你連該往哪找的線索都沒有。

這行 CSS 是怎麼混進去的?

這裡我只能講查得到的部分。

專案的 git 紀錄裡,修掉這個 bug 的那次提交(2026-08-12,加入雙語與常駐標籤側邊欄的那一包)在訊息裡寫的是:手機版 QA 時發現一個「殘留的」grid-area: sidebar,沒有對應的 grid-template-areas。ARCHITECTURE.md 的紀錄則寫,父層「從未定義」過具名區域。

有問題的那一版 CSS 沒有被單獨提交過,所以我沒辦法從歷史裡還原它是哪一刻、為什麼被寫進去的。這篇文章先前的版本對它的來歷講得比紀錄能支持的更肯定,這次我把那段拿掉了。

能確定的教訓只有一條,專案紀錄自己也是這樣寫的:grid-template-areas 跟子元素的 grid-area 是一組的,要用就整組用,不用就整組都不要留。寫下 grid-area: 某個名字 的當下,回頭確認父層真的有這個區域。刪掉父層的具名區域時,同一個 commit 把所有引用它的 grid-area 一起清掉。這跟函式和呼叫點的關係一樣,差別是 CSS 不會像編譯器那樣告訴你有一邊不見了。

下次版面歪掉,你該怎麼查?

我會照這個順序。

先別盯著截圖。 截圖只告訴你結果不對。字型、換行、寬度計算、軌道數量,在截圖上長得都一樣。

量 computed style。 一行就夠:

getComputedStyle(document.querySelector('.layout-grid')).gridTemplateColumns;

它回傳解析完的實際軌道尺寸,例如 "120.281px 0px 269.719px"。跟你寫的 1fr 一比,軌道數量對不上,你就知道問題在 grid 定位這一層,不用再往字型或內容寬度去猜。

在我這次的重現裡,這一步就是分水嶺。量到那條 0px 之前,每個假設看起來都有可能。量到之後,要找的東西只剩「是誰多開了軌道」。

把它變成回歸測試。 這類 bug 修好之後很容易在下次改版面時再犯,而它剛好很適合寫成斷言,因為軌道數量是一個乾淨的數字:

const tracks = await page.evaluate(
  () => getComputedStyle(document.querySelector('.layout-grid')).gridTemplateColumns.split(' ').length,
);
// 窄螢幕應該是單欄
expect(tracks).toBe(1);

我自己比較偏好這種測法。截圖比對會因為字型、抗鋸齒、內容更新而誤報,量軌道數量穩定得多,失敗時訊息也直接指到原因。它的盲點是只抓得到「軌道數量變了」這一種壞法,別的版面問題還是要靠別的測試。

這次重現沒有涵蓋的

講清楚這份實測的邊界,免得你把它套到不適用的地方:

  • 我只測了單一名稱的 grid-area。給四個值、或混用數字跟名稱的寫法,展開規則不同,結果我沒有量。
  • 重現用的是最簡單的兩欄 grid。巢狀 grid、subgrid、或父層有設 grid-auto-flow 的情況,軌道數量可能不一樣。
  • 三個引擎都是透過 Playwright 內建的版本跑的,而且是在桌面系統上模擬 390px 視窗,沒有在實體手機上測。
  • 專案當初紀錄的症狀(整欄被壓到逐字換行、頁高約 5000px)我這次沒有重現出一模一樣的數字,因為側邊欄內容不同。機制相同,嚴重程度取決於你的內容。

帶走一句話

CSS 沒有 undefined。你引用一個不存在的具名區域,得到的結果既不是報錯也不是忽略,是規格另外定義好的一種行為:長出隱式軌道。所有工具都會告訴你這段 CSS 沒問題,因為照規格它確實沒問題。

如果你對這種「把規格翻到底」的寫法有興趣,MySQL 分區那篇是同一個路數,只是對象換成資料庫手冊。

關於作者

我是 Ryan,RyanOps 的站主。平日的工作是軟體開發與自動化,在這裡整理 AI 模型、開發工具與軟體工程的重要變化,也記錄自己實際除錯、實作過的技術筆記。