紀錄工作經驗、相關知識,解決技術相關問題。

HTML, 新手 Jamp Holes

HTML 語意化標籤的邊界:談 section、article 與 aside 的巢狀邏輯陷阱

HTML 語意化標籤的邊界:談 section、article 與 aside 的巢狀邏輯陷阱 特色圖片

💡 文章重點摘要:當前端團隊跨越了「全頁 <div>」的初階階段後,在使用 HTML 語意化標籤時往往會掉入更隱蔽的「巢狀邏輯陷阱」——例如把全站廣告 <aside> 塞進 <article> 正文裡,讓廣告在語意上變成文章的一部分、為頁面上的十幾個 <section> 全部加上 aria-label 引發螢幕閱讀器「地標氾濫」,或是不知道 <header>、<footer> 與 <aside> 被包進區段容器後底層 ARIA 角色會發生劇變。本文將從 W3C HTML-AAM 上下文敏感語意(Context-Dependent Semantics)與 Accessible Name 計算規範切入,帶你徹底征服 <section>、<article> 與 <aside> 的進階巢狀邊界!


⚡ 快速簡單使用(30 秒複製即用範本)

如果你想立刻避開 <article>、<section> 與 <aside> 最容易踩中的巢狀語意汙染(全站側邊欄與文章附註分離、只為核心區段命名 aria-labelledby),可直接複製下方標準結構範本:

<div class="page-layout">
  <main>
    <article class="post-article">
      <header>
        <h1>文章主標題</h1>
      </header>

      <!-- 1. 一般章節 <section>:不加 aria-label,避免引發全頁 Region 地標氾濫 -->
      <section>
        <h2>第一章:核心架構</h2>
        <p>正文內容...</p>

        <!-- 2. 嵌套於 <article> 內的 <aside>:僅限放「與這篇文章直接相關」的名詞解釋或延伸閱讀 -->
        <aside aria-label="名詞補充說明">
          <p>小百科:HTML-AAM 是 W3C 定義標籤如何映射至無障礙樹的規範。</p>
        </aside>
      </section>
    </article>
  </main>

  <!-- 3. 位於 <article> 外部的頂層 <aside>:放置全站熱門排行、站內廣告等全站級補充資訊 -->
  <aside aria-label="全站熱門文章與推薦">
    <h2>熱門排行</h2>
  </aside>
</div>

💡 3 秒修改重點:

  1. 全站共用側邊欄/廣告 <aside> 必須放在 <article> 外面:<article> 代表可獨立分發的完整內容,放在裡面的廣告在語意上就成了文章的一部分;而且位於 <article> 內、沒有無障礙名稱的 <aside> 只會映射為 generic,不是 complementary 地標。
  2. 只有需要讓使用者用地標直接跳轉的核心 <section> 才加 aria-labelledby,一般章節請保持無命名狀態。

一、 為什麼標籤「放的位置不同」,底層語意就會變?上下文敏感語意揭秘

許多工程師以為一個 HTML 標籤的語意是固定不變的。然而根據 W3C HTML Accessibility API Mappings (HTML-AAM 1.0) 規範,HTML5 的區段與地標標籤具有強烈的「上下文敏感性(Context-Dependent Semantics)」——同一個標籤放在 <body> 頂層,與被嵌套進 <article>、<section>、<nav>、<aside> 等區段內容(Sectioning Content)內部時,瀏覽器賦予它的無障礙樹角色(ARIA Role)會截然不同!(<header>/<footer> 放進 <main> 內也會改變角色;<aside> 放在 <main> 內則仍是 complementary。)

為什麼標籤「放的位置不同」,底層語意就會變?上下文敏感語意揭秘 視覺化架構流程圖
圖:上下文敏感語意流程圖(圖中 <header>/<footer> 位於 <article>/<section>/<main> 內映射為 generic,為 2026-02-25 以前 HTML-AAM 的舊版映射,現行映射見下方「規範新舊差異」)

規範新舊差異:區段內 header/footer 與 aside 的現行映射

依現行 HTML-AAM 的元素映射表,上下文對角色的影響如下:

元素與位置現行映射舊版映射與變更出處
<header> 位於 <body> 頂層banner 地標未變更
<header> 位於 <main> 或區段內容內sectionheader舊版為 generic(HTML-AAM 2022-03-06 變更紀錄,GitHub PR #364);2026-02-25 w3c/aria PR #2543 合併後改為 sectionheader
<footer> 位於 <body> 頂層contentinfo 地標未變更
<footer> 位於 <main> 或區段內容內sectionfooter同上,舊版為 generic,2026-02-25 起改為 sectionfooter
<aside> 位於 <body> 或 <main> 內complementary 地標未變更
<aside> 位於區段內容內有無障礙名稱時為 complementary,否則為 generic2022-04-03 起依巢狀位置區分(HTML-AAM 變更紀錄,GitHub PR #350)

sectionheader/sectionfooter 不是地標,HTML-AAM 也註明瀏覽器與輔助科技在元素沒有無障礙名稱、不可聚焦且沒有其他可公開資訊時,可以不公開這兩個角色。重點不變:區段內的 <header>/<footer> 不會成為 banner/contentinfo 全站地標。以 Chrome 156 載入本文範例時,<article> 內的 <header> 在無障礙樹中顯示為 sectionheader。


二、 5 秒快速索引:section、article、aside 進階巢狀合法性速查表

在設計複雜版面的 DOM 結構時,請先查閱這張巢狀組合(Nesting Combinations)語意合法性與底層行為矩陣:

外層父容器 ➔ 內層子元素語法與語意合法性底層結構意義與瀏覽器解析行為真實生產環境應用場景
<article> ➔ <section>🟢 標準推薦將一篇獨立完整的文章,依主題拆分為多個有標題(<h2>~<h3>)的章節。長篇技術教學文內部的「環境建置」、「核心語法」、「踩坑要點」章節。
<section> ➔ <article>🟢 標準推薦在一個主題分組區域內,陳列多個可獨立分發(Syndicatable)的自包含內容單元。首頁「最新消息區 <section>」內含 6 張獨立新聞卡片 <article>。
<article> ➔ <article>🟡 有條件合法內層 <article> 代表與外層 <article> 內容相關的文章(附屬於主文的獨立單元)。部落格主文章 <article> 底部的單則讀者留言 <article>(嚴禁放無關的外部商品卡)。
<article> ➔ <aside>🟢 標準推薦代表與該篇 <article> 內容間接相關的補充說明(屬於文章的一部分;沒有無障礙名稱時映射為 generic,有名稱才是 complementary)。文章段落旁的「名詞解釋小辭典」、「作者引言框(Pull Quote)」、「背景數據補充」。
<body> ➔ <aside>🟢 標準推薦代表與整個網站/頁面間接相關的全站級側邊欄(role="complementary" 地標)。全站熱門文章排行榜、全站分類目錄、站內外廣告版位(不屬於單篇文章正文)。
<section> ➔ <section>🟢 合法(巢狀過深需檢討)代表大章節下的子章節(例如 <h2> 區塊內包 <h3> 子區塊)。僅限大型規格書或長文;若巢狀超過 3 層且無對應標題,通常屬於過度包裝(Over-nesting)。

三、 陷阱一:aside 放 article 裡還是放外面?失之毫釐、差之千里!

在已經熟悉 HTML5 語意化標籤基礎分界之後,工程師最常搞砸的就是 <aside> 的擺放層級。

根據 WHATWG <aside> 官方規範,<aside> 代表與「它周圍內容」間接相關的內容,所以它放在哪一層,就決定了它「依附於誰」:

  1. 當 <aside> 放在 <article> 內部時:它代表這篇文章本身的附註(例如針對文中某個專有名詞的側欄補充、勘誤表或參考文獻框),跟著文章一起被分發或重用也合理。依 HTML-AAM,這種 <aside> 有無障礙名稱時才是 complementary 地標,沒有名稱則映射為 generic。
  2. 當 <aside> 放在 <article> 外部(與 <main> 並列)時:它代表整個網頁的附屬側邊欄(例如全站導覽連結、站長簡介、Google AdSense 廣告或熱門文章推薦)。

實作程式碼對比:文章內附註 aside vs 全站側邊欄 aside

<!-- 🟢 正確示範:精準區分「文章級附註 aside」與「全站級側邊欄 aside」 -->
<div class="page-layout">
  <main class="main-column">
    <article class="tech-article">
      <h1>深入理解 V8 引擎垃圾回收機制</h1>
      <p>在 JavaScript 中,多數物件都被分配在 Heap(堆積)記憶體中...</p>

      <!-- ✅ 情境 A:位於 <article> 內部的 <aside> -->
      <!-- 語意:與本篇文章直接相關的名詞小百科,隨文章一起分發依然合理 -->
      <aside class="article-glossary-box" aria-label="專有名詞小百科">
        <h2>💡 什麼是 Stop-The-World (全停頓)?</h2>
        <p>指垃圾回收器執行標記與清理時,暫停主執行緒 JavaScript 程式碼執行的現象。</p>
      </aside>

      <section aria-labelledby="gen-gc">
        <h2 id="gen-gc">一、 分代回收假設 (Generational Hypothesis)</h2>
        <p>V8 將記憶體分為新生代(New Space)與老生代(Old Space)...</p>
      </section>
    </article>
  </main>

  <!-- ✅ 情境 B:位於 <main> 與 <article> 外部的頂層 <aside> -->
  <!-- 語意:全站共用的側邊資訊欄,與單篇文章正文無關,避免汙染文章主體與閱讀模式 -->
  <aside class="global-sidebar" aria-label="全站熱門與贊助資訊">
    <section class="widget">
      <h2>全站熱門文章排行</h2>
      <ul>
        <li><a href="/html/what-is-dom-tree-reflow-repaint-guide/">DOM 樹與渲染引擎揭秘</a></li>
      </ul>
    </section>
  </aside>
</div>

Expected Output (預期解析行為)

以 Chrome 156 載入上方範例,無障礙樹中的地標與區段節點如下:

  • main
  • article 內的 complementary "專有名詞小百科"(位於 <article> 內,因為有 aria-label 才成為 complementary;拿掉 aria-label 會變成 generic)
  • article 內的 region "一、 分代回收假設 (Generational Hypothesis)"(aria-labelledby 指向 <h2>)
  • 頁面外層的 complementary "全站熱門與贊助資訊"
  • 螢幕閱讀器地標導航:使用者可清楚區分位於文章內的「專有名詞小百科」與位於頁面外層的「全站熱門與贊助資訊」兩個補充內容地標。

四、 陷阱二:section 的「地標氾濫災難」與 Accessible Name 計算規則

這是許多中階工程師最容易誤解的規範細節。坊間常有錯誤教學宣稱:「只要寫了 <section>,螢幕閱讀器就會把它當成地標(Landmark),所以要幫每個 <section> 都加上 aria-label!」

讓我們嚴格對照 W3C 官方規範執行「去偽留真」糾正:

  1. 無名 <section> 根本不是地標!
    根據 W3C HTML-AAM 規範(section 有無障礙名稱時為 region,否則為 generic),一個沒有掛載 aria-labelledby、aria-label 或 title 的 <section>,在無障礙樹中的角色是 role="generic"(跟 <div> 一模一樣),螢幕閱讀器按地標快捷鍵時完全不會停在它上面!
  2. 幫所有 <section> 加上 aria-label 才會引發真正的災難!
    一旦你為 <section> 加上了無障礙名稱(Accessible Name),它就會瞬間升格為 role="region" 地標。如果一篇文章有 12 個小節,你把 12 個 <section> 全部綁上 aria-labelledby,視障使用者的地標選單就會被 12 個 Region 塞爆,形成地標氾濫,反而讓地標快速跳轉失去意義!W3C APG 的 Landmark Regions 對 region 的定義是「內容重要到使用者需要能直接跳轉過去的區段」,並要求頁面上有多個 region 時各自使用不同的名稱。

W3C 無障礙名稱計算(Accessible Name Computation)與地標升格決策圖

W3C 無障礙名稱計算(Accessible Name Computation)與地標升格決策圖 視覺化架構流程圖
圖:W3C 無障礙名稱計算(Accessible Name Computation)與地標升格決策圖 視覺化架構流程圖

💡 W3C Accessible Name 優先級鐵律:
依 Accessible Name and Description Computation 1.2,當一個元素同時存在多種命名來源時,瀏覽器計算無障礙名稱的優先順序為:aria-labelledby(最高優先,甚至會覆蓋 aria-label!) ➔ aria-label ➔ 宿主語言的原生標示(如 <label>、alt) ➔ title 屬性(最後備援)。region 角色不從內部文字取名,所以 <section> 裡的 <h2> 不會自動變成它的名稱。因此,只要畫面上已經有 <h2> 標題,請永遠優先使用 aria-labelledby 參照該標題的 id,絕對不要重複手寫一份 aria-label(避免日後改了 <h2> 文字卻忘記改 aria-label 造成語音與畫面不一致)。


五、 陷阱三:article 嵌套 article 的嚴格語意契約

在 WHATWG 規範中,<article> 是允許嵌套 <article> 的,但規範寫明:巢狀時,內層的 <article> 代表「原則上與外層文章內容相關」的文章,並以部落格文章底下的讀者留言作為例子。換句話說,內層的 <article> 必須在語意上與外層的 <article> 相關!

❌ 錯誤示範:把「全站推薦商品卡片」塞進技術文章 article 內部

<!-- ❌ 嚴重語意汙染:把與本文無關的電商促銷卡片塞在主文章 <article> 裡面 -->
<article class="blog-post">
  <h1>CSS Grid 網格佈局完整解析</h1>
  <p>Grid 是二維排版系統...</p>

  <!-- 💥 錯誤!這張機械鍵盤促銷卡與 CSS Grid 文章本身毫無附屬關係 -->
  <article class="promo-product-card">
    <h2>限時特價:RGB 機械式鍵盤</h2>
    <p>只要 NT$ 1,990,立即搶購!</p>
  </article>
</article>

🟢 正確示範:僅將「隸屬於該文章的讀者留言」嵌套於主 article(或抽離至平行 section)

<!-- 🟢 正確結構:內層 <article> 為針對本篇主文章的讀者回應,符合附屬關聯規範 -->
<article class="blog-post">
  <header>
    <h1>CSS Grid 網格佈局完整解析</h1>
  </header>
  <p>Grid 是二維排版系統,能同時處理欄與列...</p>

  <section class="comments-area" aria-labelledby="comments-heading">
    <h2 id="comments-heading">讀者留言討論 (2)</h2>

    <!-- 內層 article:單則讀者評論,隸屬於外層文章且具備獨立作者與時間 -->
    <article class="comment-card" id="comment-101">
      <header>
        <h3>前端小新 <span class="badge">作者認證</span></h3>
        <time datetime="2026-10-22T14:20:00">2026-10-22 14:20</time>
      </header>
      <p>感謝分享!終於搞懂 Grid 跟 Flexbox 的搭配時機了。</p>
    </article>
  </section>
</article>

六、 如何用現代 Chrome DevTools 驗證語意樹與巢狀結構?

寫完 HTML 後,請切勿使用早已過時且依據廢除演算法設計的舊式「HTML5 Outliner」擴充套件!請直接使用 Chrome 內建的 Accessibility Tree(無障礙樹檢視器)進行權威驗證:

  1. 開啟 Chrome DevTools (F12) 並切換至 Elements 面板,在 DOM 樹中選取要檢查的元素。
  2. 點擊 Accessibility 分頁(面板較窄時可能收在「更多分頁」按鈕裡)。
  3. 開啟 Show accessibility tree 開關,Elements 面板的 DOM 樹會換成整頁無障礙樹;再關掉開關即可切回 DOM 樹,原本選取的節點會保持選取。
  4. 3 秒檢查重點:
    • 觀察整頁的地標節點(banner、navigation、main、region、complementary、contentinfo)是否只出現在真正需要跳轉的區塊(避免地標氾濫)。
    • 點開每個 region 與 navigation,確認是否都有清楚的無障礙名稱(例如 region "讀者留言討論 (2)");同類型地標有多個時,名稱不要重複。

七、 工程師上線前必備:語意標籤巢狀邏輯檢驗清單 (Checklist)

  • ☐ <aside> 層級檢核:文章內部的名詞補充框放在 <article> 內;全站共用的熱門排行、分類與廣告欄 <aside> 則放在 <article> 與 <main> 外部。
  • ☐ 拒絕 Region 地標氾濫:全頁僅針對「大型核心功能區(如搜尋篩選、留言區、FAQ)」的 <section> 綁定 aria-labelledby;一般文章內文小節 <section> 僅放 <h2>/<h3>,不濫加 aria-label。
  • ☐ 優先使用 aria-labelledby:凡區塊內已有可見標題者,一律使用 aria-labelledby="標題ID" 綁定名稱,不重複手寫 aria-label 字串。
  • ☐ <article> 巢狀純淨度:主 <article> 內部絕無混入與該文章無關的外部廣告或全站推薦卡片。
  • ☐ 區段內 <header> / <footer> 不誤用:確認 <article> 或 <section> 內部的 <header> 與 <footer> 內沒有再嵌套另一個 <header> 或 <footer>(WHATWG 語法嚴格禁止 <header> 包 <header>)。

八、 參考資料與延伸閱讀

發佈留言