CSS :has() 該取代哪些 JavaScript?我會用與不會用的界線
驗證於
本文目錄
監聽 radio 的 change 事件,再替外層卡片加上 is-selected,這種程式碼在 :has() 普及後幾乎可以全部刪掉。問題是刪到哪裡該停。
我的界線是:只有外觀跟著 DOM 狀態變的,交給 :has();狀態一旦會影響行為、輔助技術讀到的內容,或表單送出的資料,就留在 JavaScript。 下面用實際例子說明這條線怎麼畫。
:has() 從 2023 年 12 月起在各主要瀏覽器都能用,MDN 標為 Baseline 廣泛可用(MDN:
直接改用 () 的三種情況
外層依內容切換版型
卡片有圖片時排成兩欄,沒有圖片時單欄。以前多半在模板裡判斷有沒有圖,再輸出不同 class;改成 CSS 後,模板就不用和樣式同步維護同一個條件。
.card { display: block;}
.card:has(> img) { display: grid; grid-template-columns: 120px 1fr; gap: 1rem;}> img 只看直接子元素。卡片內文如果夾了圖片,不會被誤判成「有封面」。
選項勾選後,外層卡片跟著變
<form> <label class="plan"><input type="radio" name="plan" value="basic"> 基本方案</label> <label class="plan"><input type="radio" name="plan" value="pro"> 進階方案</label></form>.plan { border: 1px solid #c8c8c8;}
.plan:has(input:checked) { border-color: #0064ff;}這是我最推薦的用法。勾選狀態本來就由瀏覽器管理,CSS 只是讀它。用 JavaScript 加 class 等於多養一份要同步的狀態,而且容易漏掉某些路徑:實測呼叫 form.reset() 後,radio 回到未勾選,卻沒有觸發 change 事件。靠 change 維護的 class 會停在舊狀態,:has() 的框線則會跟著恢復。
依後方元素調整間距
h2 { margin-bottom: 20px;}
h2:has(+ .subtitle) { margin-bottom: 4px;}標題後面緊接副標時縮小間距。Markdown 或 CMS 產出的內容特別適合這樣寫,因為通常改不到 HTML 結構,只能從選擇器下手。
看似能做,我仍交給 JavaScript 的情況
用樣式假裝停用的按鈕
「沒勾同意條款,送出按鈕就變淡」很容易寫成這樣:
.form:has(#agree:not(:checked)) .submit { opacity: 0.5; pointer-events: none;}畫面看起來停用了,按鈕本身並沒有。實測時這顆按鈕的 disabled 仍是 false,程式呼叫 click() 或 form.requestSubmit() 都會照樣送出表單。pointer-events 只管滑鼠、觸控這類指標操作,鍵盤使用者 Tab 到按鈕一樣按得下去,螢幕報讀軟體也會把它當成可用的按鈕。
真的要擋,就讓勾選框加上 required 交給表單驗證,或用 JavaScript 切換 disabled。:has() 可以留著做額外的視覺提示,但它擋不住送出。
需要回報展開狀態的元件
用隱藏 checkbox 搭配 :has(:checked) 做下拉選單,滑鼠點起來沒問題,輔助技術讀到的卻是一個勾選框,而不是一顆會展開選單的按鈕。這類元件我會用 <details>/<summary>、Popover API,或在 JavaScript 裡維護 aria-expanded。
錨點範圍太大的條件
開啟對話框時鎖住頁面捲動,常見寫法是 body:has(dialog[open])。MDN 的效能說明特別提到,錨點選 body、:root 或 * 時,整個子樹任何 DOM 變動都可能讓瀏覽器重新檢查條件,建議改用範圍小的容器,並在 :has() 裡用 >、+ 限縮查找範圍。
我沒有量測過這個成本在一般頁面有多大。但對話框的開關本來就有 JavaScript 處理,在同一段程式裡切換一個 class 幾乎不增加維護負擔,所以聊天室、無限捲動這類 DOM 變動頻繁的頁面,我會選擇那條路。
撰寫時會撞到的限制
:has() 不能放進另一個 :has(),裡面也不能放 ::before 這類偽元素。比較容易踩到的是錯誤的影響範圍:選擇器清單只要有一項無效,整條規則都會被丟掉。
/* .list-a 也會一起失效,因為整條規則被瀏覽器忽略 */.list-a,.list-b:has(:has(p)) { color: green;}實測時 .list-a 拿不到這個顏色。新寫的 :has() 規則沒有生效時,先把它拆成獨立規則確認是不是語法問題。
在 JavaScript 裡也能用同樣的選擇器,例如 document.querySelectorAll('.card:has(> img)')。巢狀 :has() 或偽元素丟進 querySelector() 會直接拋出 SyntaxError,比 CSS 默默忽略容易察覺。
如果專案仍要支援 2023 年底以前的瀏覽器,可以用 @supports selector(:has(*)) 包住新寫法,讓舊瀏覽器沿用原本的 class,細節可參考 @supports 的條件寫法。我會先看流量分析裡的瀏覽器版本,再決定要不要多寫這一層。
我的判斷順序
遇到「依子元素改樣式」的需求,我會依序問三個問題:
- 這個狀態是不是瀏覽器本來就知道的,例如
:checked、:focus-within、元素存不存在?是的話,用:has()讀它,比自己同步一份 class 可靠。 - 拿掉這條 CSS 之後,功能還正確嗎?如果答案是否定的,例如按鈕會變成可以送出,那它就是行為,應該由 HTML 屬性或 JavaScript 負責。
- 錨點是不是頁面上很大的容器?是的話,先縮小範圍,縮不了就改回 class。
本文範例在 2026-11-14 以 macOS 上的 Chrome 157(headless)執行,透過 getComputedStyle() 確認版型切換、勾選框線、form.reset() 後的恢復、間距、巢狀與無效清單的行為,也確認淡化按鈕仍會送出表單。其他瀏覽器沒有逐一測試。
參考資料: