主題: Web platform
<details> 不只是省幾行 JS:Disclosure 跟 Accordion 的邊界
原生 details 已經處理開關與鍵盤操作;name 能做出互斥面板,但只有內容語意與舊瀏覽器退化都合理時才該使用。
動態迷因(展開/收合)
一個 FAQ 或補充說明,常常從一個 isOpen 開始,最後長出 click handler、鍵盤邏輯、aria-expanded、動畫狀態與測試矩陣。這些不是錯;問題是,我們常在確認需求前就先重做一次瀏覽器已經會做的事。
<details> 的價值不在於「永遠不用 JavaScript」,而在於它提供一個很小、很明確的 contract:使用者按下 summary,取得額外資訊。瀏覽器負責開關、鍵盤操作與基本語意;作者只要把被揭露的內容講清楚。
我的規則是:
補充內容可以獨立展開時,先用
<details>;只有「目前開哪一項」本身就是產品狀態,或互動已經不是 disclosure 時,才做自訂 accordion。
先把它當 disclosure,不是抽屜元件
最小寫法已經有可點擊的 control。不需要替 summary 再包一個 button,也不用手動維護 aria-expanded:
<section aria-labelledby="shipping-title">
<h2 id="shipping-title">配送</h2>
<details>
<summary>何時會收到追蹤編號?</summary>
<p>包裹交給物流商後,會以電子郵件通知。</p>
</details>
</section>
這個結構適合 FAQ、技術註解、可略過的進階設定、錯誤細節與長內容中的旁支說明。它不假裝內容消失了;使用者仍看得到一個有意義的問題,決定是否展開答案。
幾個小規則比花俏動畫重要:
summary必須是第一個子元素,文字要說明「展開後會得到什麼」,不要只寫「更多」。- 把真正的 section heading 放在
summary外面。MDN 指出某些瀏覽器/輔助技術組合會讓summary內的 heading role 不一致。 - 不要把 persistent menu、刪除按鈕或其他互動控制塞進
summary。一個 trigger 裡塞另一個 trigger,通常表示這不是單純的 disclosure。
原生元素不是免檢查通行證;它只是先替你把最常見、也最容易漏掉的部分做好。
name 能做 accordion,但先問「只能開一個」是不是需求
同一組 <details> 指定相同的 name,瀏覽器會在開啟其中一項時關閉另一項:
<section aria-labelledby="plan-title">
<h2 id="plan-title">方案說明</h2>
<details name="plan">
<summary>個人方案</summary>
<p>適合單人使用與小型專案。</p>
</details>
<details name="plan">
<summary>團隊方案</summary>
<p>提供共用權限與集中管理。</p>
</details>
</section>
這是很好的小功能,但不該自動套在每一份 FAQ。讀者比對兩個答案、回頭查看步驟,或同時開著多個 troubleshooting 區塊時,「只能開一個」反而是摩擦。
選 name 前先回答一句人話:使用者打開第二項時,第一項消失會不會讓事情更好? 如果答案不是明確的會,就讓每一項獨立開著。
<details> 的基本開關已廣泛支援;name 分組則要照產品的 browser matrix 檢查。這裡很適合 progressive enhancement:舊瀏覽器若沒有互斥行為,內容仍可展開與閱讀。若「同時只能開一個」是不可妥協的工作規則,就不要把它押在一個無 JS 的便利屬性上。
動態迷因(展開/收合)
這些情況,自己做 accordion 比較誠實
HTML Standard 也提醒:<details> 代表 disclosure,不是 tabs 或 menu 的萬用替身。以下需求一出現,自訂實作通常更清楚:
| 需求 | 為何不是單純 <details> |
起點 |
|---|---|---|
| 分頁切換,內容彼此替代 | 使用者是在選取 view,不是在揭露補充資訊 | tabs pattern |
| header 旁有固定 action | 一個 summary 不該同時包住多個互動 control | heading + button + 獨立 action |
| 開啟狀態要進 URL、跨頁保存或多人同步 | open 已成為應用程式狀態 |
明確 state model |
未支援 name 時也必須永遠只開一項 |
退化成多開會破壞任務 | 自訂 accordion + 測試 |
| 需要特定方向鍵操作、focus 管理或複雜動畫 | 互動 contract 已超出原生 disclosure | 依 WAI-ARIA pattern 實作 |
這裡的重點不是「原生不夠強」,而是不要把省下的狀態管理又拿去模仿另一種 widget。當需求真的是 accordion,WAI-ARIA Authoring Practices 對 heading、button、aria-expanded、aria-controls 與鍵盤行為都有明確要求;那時候用一個可測試的 component 比在 <details> 上疊例外更便宜。
JavaScript 留給同步,不留給開關本身
如果真的需要在展開後載入昂貴資料、記錄使用情況,或同步另一個不影響核心任務的 UI,toggle event 已經是乾淨的接點:
const details = document.querySelector("#release-notes");
details.addEventListener("toggle", () => {
if (!details.open) return;
loadReleaseNotesOnce();
});
這段程式不接管開關;它只在瀏覽器已經完成狀態轉換後補上真正額外的工作。也因此測試可以先驗證內容和 keyboard toggle,再驗證資料載入是否只在需要時發生。
把 JavaScript 放在這個位置,通常比一開始就把 open 複製到 framework state 更容易維護。唯一例外是:你真的需要 framework state 成為 source of truth。那就正面承認它,別兩邊各存一份。
Code review 時只問五題
- 這段內容是補充資訊,還是另一個可選 view?
- 使用者會不會需要同時打開兩項比較或操作?
- summary 是否用可理解的文字說明展開結果?
- 不支援
name的 client,多開是否仍可完成任務? - 若答案需要自訂,是否已按真正 widget 的 a11y contract 寫測試?
<details> 最漂亮的地方,是它讓「先做內容可讀,再增加互動」變成預設。把它用在 disclosure,就能少掉一層 state 和一串 listener;需求超出時,換正確的 control。兩種選擇都比硬塞一個萬用 accordion 誠實。