vatt'ghern jaskier's ballads
本文 6 個互動圖表在手機上以重點摘要呈現,互動版請以桌面瀏覽器開啟。

替一個 box 加 8px padding,整版往右擠了 16px——這不是 bug,是 box-sizing 在 1996 年被寫進規格時就決定的事。CSS 改不掉的那些壞掉部分,幾乎全都長這個樣子:不是 implementation 的瑕疵,而是「文件排版」這個出身與「現代 UI」這個需求,在三十年後對撞留下的疤。

CSS 改不掉的那些壞掉部分——從 box-sizing 到 margin collapse 的歷史包袱

matklad(Alex Kladov,rust-analyzer 的作者)在六月四日這篇〈CSS Unavoidable Bad Parts〉裡做了一件很多「罵 CSS」的文章不做的事:他先把抱怨分成兩堆。一堆是「靠紀律可以避開」的——那些是你自己的用法問題,怪不得語言;另一堆是「源自歷史與本質、你再有紀律也躲不掉」的。這篇 deep-story 只認真處理第二堆,並且把每一個都拆到機制層,誠實標出哪些是「該認命」、哪些其實是「自己挖的坑」。

先講為什麼要做這個切分。把所有 CSS 的痛都歸到「CSS 爛」這個結論,對寫 code 的人沒有任何幫助——你下週還是得用它出貨。有用的問題是:這個痛,是我可以靠 reset、靠 box-sizing: border-box、靠少寫 selector 來消掉的,還是它焊死在 layout model 裡、我只能繞?matklad 的答案是:可避開的那堆(wrapper 地獄、far-reaching selector、vertical rhythm 的玄學)都是紀律問題;真正改不掉的,是 box-sizing 的預設值、margin collapse 的 max() 語意、font-size 量的那個鬆散虛擬框、混字體時錯位的 line-box、word-break 只認 whitespace 與 hyphenation 的那條規則,以及一個更上層的事實——「那個通用的 layout algorithm 並不存在」

這些東西的共同根源,他講得很直白:CSS 早年是為「文件排版/紙張隱喻」設計的。它假設你在排一篇會從上往下流的文章,假設 margin 是排版師在紙上留白的概念,假設一行字就是一行字。等到大家拿它來蓋 application UI——要對齊、要等高、要在固定視窗裡塞滿不可換行的 identifier——這套紙張假設處處露餡。下面逐個拆。

沒有那個 layout algorithm

最上層、也最容易被忽略的「壞掉部分」,其實是一個哲學層級的事實:CSS 沒有單一、通用的 layout algorithm。matklad 的原話是「there isn't a fully general solution to positioning and sizing GUI boxes」——把一堆方框擺進一個容器、決定它們各自的位置與大小,這件事本身就沒有一個放諸四海皆準的解。不同系統用不同的啟發法,因為「the layout algorithm doesn't exist」。

CSS 把這個事實外顯成一堆並存、彼此規則不相容的 formatting context:normal flow(block 與 inline)、float、table、flexbox、grid。它們不是同一個演算法的不同參數,而是各有各的盒子量測規則、各有各的「誰決定誰的尺寸」的因果方向。block 是「寬度由 containing block 給、高度由內容撐」;flex 是「main axis 先分、cross axis 後拉」;grid 是「先解 track、再放 item」;table 還有它自己那套兩遍掃描的欄寬演算法。你以為你在學「CSS layout」,其實你在學五套互不通約的方言。

這就是為什麼「我這個東西該用 flex 還是 grid」永遠沒有乾淨答案——不是你不夠熟,是這兩套演算法對「容器與子元素」的尺寸協商方向根本不同,於是同一個視覺結果可以由完全不同的機制達成,而它們的 edge case 完全不一樣。下面這張圖把幾個 context 並排,點任一個看它各自「誰決定誰的尺寸」。

click any context to read its sizing rule · 5 formatting contexts

五套互不通約的 sizing 方言 block flow 寬給定 · 高撐開 inline flow 沿 baseline 串接 flexbox main 分 · cross 拉 grid 先解 track · 再放 item table 兩遍掃描定欄寬 同一個視覺結果,可由互不相容的演算法達成

block flow · 尺寸規則

寬度由 containing block 給(預設撐滿可用寬),高度由內容由上往下堆疊撐開。因果方向是「外決定寬、內決定高」。

這是紙張隱喻最純粹的殘留:文章從上往下流,一段接一段。它天生 reflow,這也是後面會講的「HTML 本來就 responsive」的來源。

inline flow · 尺寸規則

沿著 baseline 把字形與 inline box 一個接一個串起來,到行尾換行。盒子的高度由 line-height 與字體的虛擬框決定——不是由字形本身。

這就是混字體時 line-box 會錯位、整行被撐高的根。inline 是 CSS 最「排版」的一層,也最不像 GUI。

flexbox · 尺寸規則

先沿 main axis 依 flex-grow / shrink / basis 分配剩餘空間,再處理 cross axis 的對齊與拉伸。容器與子元素的尺寸是雙向協商,不是單向給定。

跟 block 的「外決定寬」剛好相反——子元素的內容寬度會回頭影響分配。這正是兩套演算法不通約的地方。

grid · 尺寸規則

先把 track(行與列的尺寸)解出來,再把 item 放進 cell。fr 單位、minmax()、auto-placement 都是在 track 解算階段做的。

跟 flex 的差異不在「橫 vs 縱」,而在尺寸決定的時序:grid 先有格線、後有內容;flex 先有內容、後分空間。

table · 尺寸規則

欄寬要掃兩遍:先量每個 cell 的 min/max content,再依 table 寬度分配。這是最古老、也最容易在 table-layout: auto 下產生意外的演算法。

它存在純粹因為歷史:早年大家用 table 排版。它跟前四套又是另一個世界的規則。

沒有一個 root 演算法把這五套統一起來;它們是並存的方言。學 CSS layout,本質上是學會在哪個 context 裡你在跟哪一套規則談判。

沒有一個 root 演算法把這五套統一起來;它們是並存的方言

CSS 五套 formatting context(block、flex、grid、table、inline)的 sizing 規則互不相通,沒有統一演算法。

把同一件事換個角度看會更清楚:layout 的核心難點是「尺寸的因果方向」。在 block flow 裡,因果是單向的——containing block 由外往內把寬度交給你,你只能往下撐高度,所以排版器可以一遍掃完。一旦進到 flex 或 grid,因果變成雙向:子元素的 intrinsic content size(它「最少需要多寬」與「最多想要多寬」)會回頭影響容器怎麼分配空間,而容器分配完又回頭限制子元素。雙向約束沒有單遍解,瀏覽器得跑 min-content / max-content 兩種量測、有時還要多遍 reflow 才收斂。這就是為什麼 flex 的 flex-basis: automin-width: auto 會產生那個惡名昭彰的「flex item 不肯縮小」問題——它的 implied minimum 是內容的 min-content,而不是 0。

matklad 把這件事歸到「認命」那一側,我同意。這不是 CSS 設計者偷懶——是「把方框擺進方框」這個問題在數學上就沒有單一解,任何 GUI 工具鏈(包括原生的 Auto Layout、Flutter 的 constraint、Android 的 ConstraintLayout)都得在某處選一套啟發法。CSS 的特殊之處只是它把好幾套都暴露在同一個 property 命名空間裡,於是你得隨時知道自己在哪一層。一個元素的 display 不只是「它長什麼樣」,更是「它與它的子元素活在哪一套尺寸演算法裡」——這個雙重語意,正是初學者最常踩空的地方。

box-sizing:加 padding 就位移版面

接著是最常被當「新手入門第一坑」的 box-sizing。預設值是 content-box,意思是你寫的 widthheight 不包含 border 與 padding。用 matklad 的話:「width and height do not include element's border and padding」,於是「increasing padding somewhere shifts the entire layout unexpectedly」——在某處加一點 padding,整個版面就意外位移。

具體機制是這樣:你給一個 box 設 width: 200px,再加 padding: 0 16px,它在 content-box 下實際佔的水平空間是 200 + 16 + 16 = 232px。你以為你在「box 內部留白」,實際上你在「把 box 撐大 32px」,於是它的鄰居被往外推。把模式切成 border-boxwidth: 200px 就是連 border 與 padding 在內的最終寬度,padding 從內容裡吃、不往外長。下面這個 demo 讓你直接拉 padding 與 border,看兩種模式下「你寫的 width」與「實際佔位」怎麼分岔。

為什麼這在 component 化的世界裡特別致命?因為元件的痛點是「可組合性」。當你寫一個 Card 元件、宣告它寬 100%、內距 16px,你預期的是「它填滿父容器、內容離邊 16px」。content-box 卻給你「它填滿父容器再加 32px」,於是放進一個剛好 100% 寬的格子裡,它溢出。你會發現自己被迫去算 width: calc(100% - 32px) 這種補償式運算——而這正是 box model 在跟你的心智模型作對。border-box 之所以幾乎變成業界共識,不是因為它「更對」,而是因為它讓「我宣告的尺寸=這個盒子對外承諾的尺寸」,把 padding 變成元件的內部私事、不外溢成鄰居的問題。可組合性要求的是封裝,content-box 恰恰破壞封裝。

drag padding / border and toggle box-sizing · watch the footprint shift

16px
4px
declared width = 200px(圖中固定刻度) available track(400px) 你寫的 width = 200 content 鄰居 實際佔位 footprint = 232px content 實際寬度 = 200px content-box:padding 往外長,鄰居被推開
同一條 declared width = 200 的虛線基準。content-box 下,footprint = 200 + 2×padding + 2×border,鄰居隨之位移;border-box 下,footprint 永遠 = 200,是 content 被往內擠。這就是為什麼幾乎所有 reset 第一行都是 * { box-sizing: border-box }

同一條 declared width = 200 的虛線基準

content-box 把 padding 和 border 加在宣告寬度之外導致版面位移;border-box 把 footprint 鎖在宣告寬度。

該認命還是自挖坑?matklad 把它放在「歷史包袱」那側——預設值是 content-box 而非 border-box,純粹是因為早年 CSS Box Model 規格這樣定,而 IE 早期甚至實作成另一套(quirks mode 的「IE box model」其實更接近 border-box,諷刺的是當年被當成 bug)。今天你幾乎一定會在 reset 裡把所有東西改成 border-box,但你必須記得改——預設值改不動,因為改了會打爛整個 web 既有的版面。這是典型的「焊死的歷史」:機制本身可以更好,但相容性把它鎖在壞掉的預設上。

margin collapse:用 max() 不是 sum()

下一個是 margin collapsing。兩個垂直相鄰的 margin 碰在一起時,最終的間距不是兩者相加,而是兩者取最大值。matklad 的描述:「two neighboring margins are combined using max rather than sum」,而且這「can surprise you」——會在你毫無預期時嚇你一跳。

為什麼是 max() 不是 sum()?這又是紙張隱喻:排版師在想「這個標題下方至少要留 20px」「這段文字上方至少要留 16px」,當兩個「至少」碰在一起,排版直覺是「滿足比較大的那個就好」,而不是「兩個需求加起來」。對連續文章流,這其實相當合理——它讓你不必去算「上一段的 margin-bottom 加這一段的 margin-top」這種脆弱的總和。但對 component 化的 UI,它是惡夢:你給一張 card 設 margin-top: 24px,它的間距卻可能被相鄰元素的 margin「吃掉」,因為系統只取了 max。下面這個 demo 讓你各自拉兩個相鄰 margin,並切換 max()(真實 CSS 行為)與 sum()(你直覺以為的行為),看那條間距怎麼變。

更陰險的是 margin collapse 還有兩個你常忘記的變體。第一個是「父子之間」也會 collapse:如果父元素沒有 border、padding、也沒有建立 BFC,子元素的 margin-top 會「穿透」父元素跑到外面去,造成「我明明在子元素上設了上邊距,結果整個父容器往下掉」的詭異現象。第二個是「空元素自我 collapse」:一個高度為 0、沒有內容的元素,它的上下 margin 會合併成一個。這兩個變體都源自同一個排版假設——margin 是「元素之間的留白需求」,而不是「元素自己佔的實體空間」。把這個本質記牢,這些 surprise 就從「玄學」變成「可推導」:問自己「這裡有沒有東西阻止兩個 margin 碰面(border / padding / BFC)」,沒有就會 collapse。

drag both margins, toggle max() vs sum() · watch the gap

32px
20px
box A mb 32 mt 20 box B 32 gap = max(32, 20) = 32px 較小的那個 margin 被完全吸收,毫無貢獻
真實 CSS 取 max():兩個 margin 重疊,較小的被較大的完全吃掉。切到 sum() 看你直覺的版本——也是大多數 component 作者第一次踩坑時以為應該發生的版本。對連續文章流 max() 合理,對 UI 拼裝是 surprise 的來源。

真實 CSS 取 max():兩個 margin 重疊,較小的被較大的完全吃掉

CSS 的垂直 margin 以 max() 而非 sum() 合併,較小的那個被完全吸收而無任何貢獻。

這個我會把它放在「半認命」:max() 的選擇在文件排版脈絡下確實是對的設計,不是純粹的錯誤。但現代解法是——別用 margin 做元素之間的間距。改用 flex / grid 的 gap,gap 永遠是 sum 語意、不 collapse、不會穿透父層。matklad 與多數現代 CSS 寫法都往這個方向走:margin 留給「元素內部與外界的最小留白」這種真正排版的場合,元素之間的節奏交給 gap。這是少數「壞掉部分」有乾淨繞法的例子。

font-size 量的不是字形,是一個鬆散的虛擬框

再往下是字體。這裡有個幾乎每個前端都「知道但說不清」的事:font-size: 16px 並不是說「字形高 16px」。matklad 講得很精準——font-size 是「the size of a virtual box around the glyph, but the box isn't tight, and the size of the glyph varies, depending on the font」:它是字形周圍一個虛擬框的尺寸,但這個框並不貼合字形,而且實際字形的大小還會隨字體不同而變。

後果是兩個同樣 font-size: 16px 的字體,視覺大小可以差很多——因為各自字體的 em box 內,字形佔的比例(x-height、cap-height 與留白)都不同。這也是為什麼設計稿換字體後常要重調 size。下面這張靜態圖把「em box(你設的 16px)」「字形實際輪廓」「baseline / x-height / cap-height」三者疊起來,讓你看清 font-size 量的是哪個框、字形又落在框的哪裡。

font-size = em box 的高,不是字形的高 字體 A(x-height 較小) em box(= font-size) ascent cap-height x-height baseline descent x 字體 B(x-height 較大) x-height(更高) x 兩邊 em box 一樣高(同一個 font-size),但字形實際視覺大小差一截 框不貼合字形,slack 的多寡由字體決定——這就是「font-size 量的不是字形」
同一個 font-size,字體 A 的字形在 em box 裡留下大量上下 slack,字體 B 幾乎撐滿。設計師說「換了字後字看起來變小」不是錯覺——font-size 鎖的是框,字形在框裡的比例由字體自己定。

同一個 font-size,字體 A 的字形在 em box 裡留下大量上下 slack,字體 B 幾乎撐滿

font-size 量的是字形外圍的鬆散 em box,不是字形本身,同一大小在不同字體的視覺尺寸差異顯著。

這也解釋了一個常被誤解的事:line-height: 1.5 的那個 1.5 是乘在 font-size 上,而 font-size 量的是 em box,不是字形。於是「行高 1.5」並不是「行間留白等於 0.5 倍字高」這種你以為的乾淨關係——它是「em box 的 1.5 倍」,而字形在 em box 裡本來就帶著不對稱的 slack。額外的 leading(line-height 減去 font-size 那部分)會被對半分到字形上下,但因為字形本身在框裡就不置中,最終的視覺行距常常上下不均。要做到 baseline 對齊的精準排版,你得回去查字體的實際 metrics,而不是相信 font-size 與 line-height 的算術。

把這件事接到行高,就引出下一個壞掉部分。既然每個字形帶著自己的 em box 與 ascent/descent,那麼當你在同一行裡混用兩種字體(最常見:中文正文配英文 inline code),每個 inline box 會依各自字體的 metrics 去算自己的 line-box 高度。matklad 點出的後果是:line-box 之間會相對位移(「line-boxes get shifted relative to each other」),整行的 line height 被那個最「高」的字體撐大,於是你會看到「明明只插了一小段 code,這一行卻比上下行高」。

這個的根還是 inline formatting:CSS 算行高,是把行內每個 box 依各自 metrics 對齊到共同 baseline,再取聯集當作 line-box 的高。混字體=混 metrics=聯集被拉大。能不能避開?部分能——統一字體、或在 inline code 上設一致的 line-heightvertical-align 收斂位移;但只要你在一行裡放兩種 metrics 不同的字體,本質的錯位就回不去乾淨。這條我算「該認命,但可緩解」。

word-break:手機上橫向溢出的元兇

接著是一個在手機上最有感的壞掉部分。CSS 預設斷行的規則是:只能在 whitespace 或 hyphenation point 換行。matklad 的話:「you can only break the line at the whitespace, or at the hyphenation points」。對一般英文散文這沒問題——詞與詞之間有空白。但一旦你放一段沒有空白的長字串:一個 inline code 的長 identifier、一條完整 URL,CSS 找不到可斷點,於是那一整串拒絕換行,在窄視窗(手機)裡直接撐破容器、造成整頁橫向溢出。

這是「文件排版假設」最赤裸的露餡:紙張上沒有「視窗寬度會變」這件事,所以原始模型沒有預設「遇到不可斷的長串該怎麼辦」。下面這個 demo 讓你把視窗寬度(用 slider 模擬)往手機方向拉,看一段含長 URL 的內容在預設斷行下怎麼溢出、以及加上 overflow-wrap: anywhere 後怎麼被救回。

值得分清楚的是這幾個相關 property 的差別,因為它們常被混用。overflow-wrap: anywhere(舊名 word-wrap: break-word)只在「整行放不下」時才允許在詞中斷,且它在計算 min-content 寬度時會把長字當作可斷——對 flex/grid 的尺寸協商比較友善。word-break: break-all 則更激進,任何時候都允許在任意字元斷,連空間夠的時候也會。hyphens: auto 又是另一回事,它只在合法的 hyphenation point 插連字號、且需要 lang 屬性與字典支援。對「容器裡可能出現長 URL 或長 identifier」這個最常見的爆版情境,正解幾乎總是 overflow-wrap: anywhere——它只在必要時出手,不會把正常英文詞也切得支離破碎。記住這個對應,就能在 code review 時一眼看出別人用錯了 property。

shrink the simulated viewport · watch the URL overflow then get saved

240px
simulated viewport · 240px 預設:長 URL 找不到斷點,衝出右邊界
內容是一段散文加一條長 URL(https://example.com/very/long/path?token=…)。預設模式下 URL 是單一不可斷 token,窄視窗時溢出邊界(紅色虛線右側);切到 anywhere,CSS 被允許在任意字元處斷,文字重新收進框內。手機上整頁橫向 scroll 的元兇,十之八九是這個。

內容是一段散文加一條長 URL(https://example.com/very/long/path?token=…)

預設斷行規則只認 whitespace,沒有空白的長 URL 在窄視窗直接溢出;overflow-wrap:anywhere 能修復。

這條我會明確標成「該認命的本質問題、但有標準解」。本質上,「一個比容器還寬、又沒有合法斷點的 token,該怎麼辦」沒有不痛的答案——你要嘛允許在不該斷的地方斷(破壞可讀性),要嘛讓它溢出(破壞版面)。CSS 後來補了 overflow-wrapword-breakhyphens 一整排 property 讓你選毒藥,但「預設行為會在手機上爆版」這件事改不掉,因為改預設又會打爛既有 web。每個前端都該把「overflow-wrap: anywhere 給可能含長 URL/長 identifier 的容器」當成肌肉記憶。

browser default 與 cascade:兩種不同的「歷史稅」

還有兩個更瀰漫、不對應單一 property 的壞掉部分。第一個是 browser default 的不一致:每個瀏覽器對 heading margin、list padding、form control 外觀的預設值都不完全一樣。matklad 點出問題的微妙之處——這些不一致出現在「something you didn't write」,是你沒寫過的東西在咬你。reset/normalize 之所以是每個專案的第一個 import,就是在抹平這層你沒寫卻要負責的差異。這嚴格說不是 CSS 語言的錯,是「多家獨立實作同一份模糊規格」的歷史結果,但對寫 code 的人,它是實打實的稅。

第二個是 cascade 與 selector 本身。matklad 把它形容成「supercharged inheritance + monkey patch」——一個被加了渦輪的繼承機制,疊上隨處可以 monkey patch 的能力。任何一條 selector 都可能在你不知情的地方改掉某個元素的樣式,specificity 決定誰贏,而這個「誰贏」常常違反你閱讀 source 的直覺。這是 CSS 全域、宣告式、後到不一定後贏(看 specificity)的本質。

「supercharged inheritance」這半句也值得拆。CSS 的 inheritance 本身就比一般程式語言的繼承更激進:colorfont-familyline-height 這些屬性會自動往下傳給所有後代,你不寫就繼承。這在排版脈絡下是恩賜——設一次正文字體,整篇都有了。但它也意味著任何一個祖先元素的改動,都可能在很深的後代身上產生你沒預期的效果,而你 debug 時得沿著整條 DOM 祖先鏈往上找「這個值是從哪繼承來的」。「monkey patch」那半句則是指 selector 的全域性:任何一條 .foo .bar 都在對「整個文件裡所有符合的元素」下手,沒有模組邊界。兩者疊起來,就是 matklad 說的那把渦輪增壓的槍。

不過 matklad 把 cascade 主要放在「可避開」那一側:少寫 far-reaching selector、用 CSS nesting 把作用域收窄、甚至走 classless + inline style 的極端,都能讓你大幅躲開 monkey-patch 地獄。換句話說,cascade 給你一把會走火的槍,但你可以選擇不亂揮。現代的 scoped styles、CSS Modules、@scope 都是在制度層面把這把槍收進槍套。所以這條的判定是:機制有歷史包袱,但痛苦程度高度取決於你的紀律——大半是自挖的坑。

responsive design:很多複雜是自找的

最後一個是 matklad 最反直覺、也最容易引戰的論點:很多人投入大量 @media query 與斷點維護的 responsive design,其實是自找的複雜。他的根據是一句常被忘記的事實——「HTML is inherently responsive」。一份只有 block 與 inline 的純 HTML,丟進任何寬度的視窗,本來就會自動 reflow:文字依容器寬度換行,block 依序往下堆,窄視窗自動變窄欄。responsive 不是你加上去的功能,是 normal flow 的預設行為。

那 @media query 在解決什麼?通常是在解決「你先用 fixed width、absolute positioning、或把版面寫死成桌面尺寸」之後,把它救回流式的問題。你先親手把天生會 reflow 的東西釘死,然後寫一堆斷點把它在不同尺寸下重新拆活。matklad 的觀察是:如果你一開始就順著 normal flow 寫、少釘死尺寸、用 max-width 而非 width、讓內容自己決定換行,你需要的 media query 會少非常多。

把這個論點推到具體技術上,會發現它其實預言了 CSS 近幾年的演化方向。flex-wrap: wrapflex-basis 可以讓一排卡片在空間不足時自己換行成多行,不需要任何斷點;grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)) 這一行可以讓格線數量隨容器寬度自動增減,達成過去要三四個 @media 才能做的「桌面四欄、平板兩欄、手機一欄」。再加上 clamp() 讓字級與間距隨視窗連續縮放、container query 讓元件依「自己所在容器」而非「整個視窗」反應——這些工具的共同主題,正是把 responsive 從「在斷點處手動切換固定版面」拉回「讓內容與容器連續協商」。換句話說,現代 CSS 的進展,很大一部分就是在還 normal flow 那份「天生 responsive」的債。

下面這個 before/after 把同一段內容用兩種寫法疊起來:左側「fixed-width,需要斷點救」,右側「順流式,天生 reflow」。拖中間的分隔線比較它們在被擠窄時的反應——一邊溢出待救,一邊自己收好。

drag the divider · fixed-width-needs-breakpoints vs flows-by-default

fixed-width,需斷點救 width: 360px; /* 寫死 */ 容器邊界 → 內容衝出邊界, 得靠 @media 重排救回 每個斷點都是手工維護的負債 順流式,天生 reflow max-width: 100%; 內容依容器自己換行 block 依序往下堆 幾乎不需要 media query normal flow 的預設行為
左:先把寬度釘死,再用斷點救活;右:順著 normal flow,內容天生隨容器收放。matklad 的論點不是「不要 responsive」,而是「你的 responsive 複雜度,很多是先破壞 reflow、再花力氣修回 reflow 製造出來的」。

左:先把寬度釘死,再用斷點救活;右:順著 normal flow,內容天生隨容器收放

HTML normal flow 天生 responsive;fixed-width 要靠 @media 斷點補救是先破壞 reflow 再修回的自挖坑。

這條最該打星號。matklad 自己也承認這是個有爭議、帶個人風格的主張——對內容導向的網站(部落格、文件、新聞)他的論點極強:你真的不太需要斷點。但對真正的 application UI(dashboard、編輯器、需要在桌面與手機呈現完全不同資訊密度的介面),「天生 reflow」遠遠不夠,你確實需要 container query 與斷點去做結構性的重排,而不只是讓文字換行。所以這條的誠實判定是:對文件,是自挖的坑;對 app,是真需求——把這兩種情境混為一談,正是很多關於「CSS 該不該這麼複雜」的爭論談不攏的原因。

該認命 vs 自挖的坑:一張總帳

把七個壞掉部分按 matklad 的切分整理成一句話的判決,會看到一個清楚的譜系:

幾乎純粹該認命(焊死的歷史 / 本質難題):layout algorithm 不存在——這是數學事實,任何 GUI 系統都逃不掉;font-size 量鬆散虛擬框、混字體 line-box 錯位——這是字體排版的本質,可緩解不可消除;word-break 預設爆版——本質難題加上「改預設會打爛 web」的相容性鎖。

半認命(設計在原脈絡下合理,但有現代繞法):box-sizing 預設 content-box——機制可以更好,被相容性鎖在壞預設,但你一行 reset 就繞過;margin collapse 用 max()——對文件排版是對的設計,對 UI 改用 gap 就乾淨繞開。

大半是自挖的坑(紀律問題):cascade 的 monkey-patch 地獄——少寫 far-reaching selector、用 nesting/scope 收窄就大幅緩解;responsive 的斷點負債——對內容站,順著 normal flow 寫就幾乎不需要,複雜度多半是先破壞 reflow 再修回來製造的。介於中間的是 browser default 不一致——不是 CSS 語言的錯,是多家實作的歷史稅,但你必然得付(一個 reset)。

這個切分對你下週寫 code 的實際意義是:當你又被 CSS 咬一口,先問自己落在譜系哪一格。如果是「該認命」那格,別浪費力氣對抗機制,學會它的形狀、用標準緩解手段(reset、border-box、overflow-wrap、gap)把痛降到最低;如果是「自挖的坑」那格,那是個信號——你該回頭檢查自己的寫法(是不是釘死了尺寸、是不是寫了過寬的 selector),而不是再加一層 workaround。把「機制的痛」與「我的痛」分開,是 matklad 這篇文章真正的價值,遠比「CSS 爛不爛」這個沒有出口的爭論有用。

The model:CSS 改不掉的那些壞掉部分,幾乎全是「文件排版出身」撞上「現代 UI 需求」的疤——box-sizing、margin collapse、font 與 line-box、word-break 都是紙張隱喻在動態視窗下的失靈。但同一篇文章也提醒:cascade 與 responsive 的多數痛苦是紀律問題,不是語言問題。會用 CSS 的人,差別不在背了多少 property,而在每次被咬時能瞬間分辨——這口是焊死的歷史,還是我自己挖的坑。