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

HTML, 新手 Jamp Holes

HTML 語意化教學:main vs div 差異比較與標準雙欄佈局範本

HTML 語意化教學:main vs div 差異比較與標準雙欄佈局範本 特色圖片

在實踐 HTML 語意化(Semantic HTML)時,許多新手開發者為了減少 <div> 的數量,習慣把 <main> 當成全頁最外層的萬用容器(Wrapper),將頂部 <header>、側邊欄 <aside> 與底部 <footer> 全部包進 <main> 裡面。然而,根據 W3C HTML-AAM 1.0 官方規範,一旦把 <header> 與 <footer> 放進 <main> 內部,它們在無障礙樹中的 banner 與 contentinfo 地標角色就會自動失效並降級為普通的 generic 容器!本文將帶你釐清 <main> 與 <div> 的職責邊界,並提供符合標準的現代 CSS Grid 佈局範本。

在進行前端專案的程式碼審查(Code Review)時,我們常看到兩種極端:一種是整頁滿滿的 <div>(俗稱 Div Soup);另一種則是「過度語意化」——誤以為 <div> 是落伍的標籤,進而把原本應該用 <div class="app-layout"> 或 <div class="container"> 處理的純排版外框,全部替換成 <main>。

事實上,在 HTML 語意化 的標準架構中,<div> 從來沒有被淘汰。語意化標籤負責傳遞「文件結構與無障礙地標(Landmarks)」,而 <div> 則專責處理「視覺寬度限制、Flexbox / Grid 網格對齊與背景裝飾」。以下先提供 30 秒即可複製套用的標準雙欄佈局範本,再逐步拆解為什麼不該把 <main> 當成 <div> 使用。


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

當你需要實作包含「全站頁首、中央主內容區、右側邊欄與全站頁尾」,且希望在內容較少時頁尾依然自動貼齊視窗底部(Sticky Footer)的版面時,請遵循以下「外層排版交給 div,核心區塊交給語意標籤」的標準結構:

<!DOCTYPE html>
<html lang="zh-Hant-TW">
<head>
  <meta charset="UTF-8">
  <style>
    body {
      margin: 0;
    }

    /* 1. 全頁最外層排版外框:使用純樣式容器 div.page-shell,切勿用 <main> 包住全頁 */
    .page-shell {
      min-height: 100dvh;
      display: grid;
      grid-template-rows: auto 1fr auto;
    }

    /* 2. 中央雙欄寬度限制與網格容器:使用純樣式容器 div.layout-container */
    .layout-container {
      width: min(100% - 2rem, 1200px);
      margin-inline: auto;
      display: grid;
      grid-template-columns: 1fr 300px;
      gap: 2rem;
      padding-block: 2rem;
    }

    @media (max-width: 768px) {
      .layout-container {
        grid-template-columns: 1fr;
      }
    }
  </style>
</head>
<body>
  <!-- ✅ 外層排版容器使用 div,確保內部四大無障礙地標各自獨立平級 -->
  <div class="page-shell">
    <!-- 地標 1:頂層 header (自動映射 role="banner") -->
    <header class="site-header">
      <a href="#main-content" class="skip-link">跳至主要內容</a>
      <nav aria-label="全站主選單">導覽列內容</nav>
    </header>

    <!-- 雙欄排版外框使用 div,讓 main 與 aside 保持平行 -->
    <div class="layout-container">
      <!-- 地標 2:全頁唯一主要內容區 main (自動映射 role="main") -->
      <main id="main-content" tabindex="-1">
        <article>
          <h1>文章主標題:這裡是該頁面獨有的核心內容</h1>
          <p>正文段落...</p>
        </article>
      </main>

      <!-- 地標 3:頂層側邊欄 aside (自動映射 role="complementary") -->
      <aside aria-label="熱門文章與延伸資源">
        <h2>熱門文章排行</h2>
      </aside>
    </div>

    <!-- 地標 4:頂層 footer (自動映射 role="contentinfo") -->
    <footer class="site-footer">
      <p>© 2026 Sean 技術筆記. All rights reserved.</p>
    </footer>
  </div>
</body>
</html>

一、 5 秒快速索引:main vs. div vs. section vs. article 職責對照表

為了避免在切版時混淆「語意地標」與「排版容器」,以下整理 WHATWG HTML Living Standard 與 W3C HTML-AAM 1.0 對四大常見容器的定義:

容器標籤W3C 隱含無障礙角色 (Implicit Role)語意定位與核心職責數量與階層限制典型適用場景
<div>generic(無地標語意)純視覺排版與樣式分組容器(Styling & Layout Hook)無數量與階層限制(可合法包覆 <header>、<main>、<footer>)全頁外框 .app-wrapper、最大寬度置中 .container、Flex/Grid 列容器
<main>main(主要內容地標)單頁獨有的核心主體內容(排除跨頁重複之選單、側欄、頁尾)同一時間最多 1 個可見(詳見《HTML main 標籤的唯一性迷思》);祖先僅限 html/body/div/無名 form包覆整頁的核心文章 <article>、商品詳情區、後台儀表板主畫面
<article>article(獨立文章項目)具備獨立完整性、可單獨分發或轉載的內容單元可有多個;可互相嵌套(如主文章內含多則讀者留言)部落格單篇文章、新聞報導、論壇貼文、商品評論卡片
<section>無命名時為 generic;綁定標題命名後升格為 region 地標文件或文章內部具備明確主題(Thematic Grouping)的章節區塊應包含 <h2>~<h6> 章節標題;不應拿來當純 CSS 外框長文中的各個大章節、首頁的「產品特色區」、「常見問答區」

二、 為什麼把 main 當成全頁最外層 div 會破壞無障礙地標?

許多初學者在建立 React、Vue 或一般 HTML 專案時,看到根節點 <div id="app"> 底下需要一個包住整張頁面的 Flex/Grid 容器,就直接寫成 <main class="app-wrapper">,然後把全站 <header>、<aside> 與 <footer> 統統塞進 <main> 裡面。

從視覺畫面上看,瀏覽器對 <main> 和 <div> 的預設樣式幾乎一樣(皆為 display: block),所以版面不會破版;但在瀏覽器底層的 無障礙樹(Accessibility Tree) 中,這個寫法會直接引發連鎖的語意降級:

  1. 頂層 <header> 與 <footer> 的地標角色直接消失:
    根據 W3C HTML-AAM 1.0 (§4.3 header & footer) 規範,<header> 唯有在祖先不包含 <article>、<aside>、<footer>、<nav>、<section> 與 <main> 時,才會映射為全站橫幅地標 role="banner";同理,<footer> 也唯有在不在 <main> 等元素內部時,才會映射為頁尾地標 role="contentinfo"。
    一旦你把 <header> 與 <footer> 包進 <main> 裡面,瀏覽器就會認為它們只是某個局部區塊的標頭與註腳,自動將它們降級為 role="generic"(等同於普通 <div>),導致視障使用者無法透過螢幕閱讀器地標快捷鍵找到全站頁首與頁尾!
  2. 「跳至主要內容(Skip Link)」失去意義:
    頁首的第一顆無障礙連結 <a href="#main-content">跳至主要內容</a>,目的是幫助鍵盤使用者跳過 <header> 裡數十個重複選單連結,直接抵達正文。如果你把 <header> 也包在 <main id="main-content"> 裡面,按下跳轉後焦點依然停在最頂端的導覽列前,完全喪失了繞過重複區塊(WCAG 2.2 SC 2.4.1 Bypass Blocks)的功能。

請透過下圖對比兩種結構在無障礙樹中的差異:

圖 1:將 main 誤當成全頁外框容器導致 header 與 footer 無障礙地標自動降級之結構對比圖
圖 1:將 main 誤當成全頁外框容器導致 header 與 footer 無障礙地標自動降級之結構對比圖

如需進一步了解無障礙樹的結構脈絡,可參考 圖 1:將 main 誤當成全頁外框容器導致 header 與 footer 無障礙地標自動降級之結構對比圖 的完整視覺化對照。


三、 實務切版常見的 3 大語意化佈局誤區與正確選型

為協助開發者在各種排版情境下做出正確判斷,可先參考 圖 2:HTML 語意化佈局容器選型決策樹(main vs. article vs. section vs. div) 的視覺決策路徑,在 3 秒內決定該用 <main>、<article>、<section> 還是 <div>:

圖 2:HTML 語意化佈局容器選型決策樹(main vs. article vs. section vs. div)
圖 2:HTML 語意化佈局容器選型決策樹(main vs. article vs. section vs. div)

以下拆解三個在 Code Review 中最常遇到的佈局誤區:

誤區 1:為了實現「最大寬度置中(.container)」或「雙欄網格」,把 aside 塞進 main 裡

在設計「左側文章正文(佔 70% 寬)+ 右側全站熱門文章側邊欄(佔 30% 寬)」的版面時,因為兩者需要共用一個 max-width: 1200px; display: grid; 的父容器,許多人會直接把 <main> 當成這個雙欄父容器,將全站共用的 <aside> 寫在 <main> 裡面。

  • 規範分析:根據 WHATWG §4.4.14,<main> 僅能包含「該文件獨有的主要內容」,不應包含跨頁面重複出現的側邊欄、站內搜尋框或全站導覽列。此外,根據 W3C ARIA Landmarks Pattern,代表全站側邊欄的 <aside>(role="complementary")應作為頂層地標,與 <main> 保持平行(Sibling)關係。
  • 正確寫法:在外層使用一個純排版用的 <div class="layout-container"> 負責 max-width: 1200px 與 display: grid,並在裡面平行放置 <main> 與 <aside>。

誤區 2:在列表頁把每一張商品卡片或文章預覽卡都寫成 main

有些新手誤以為「這張卡片裡的主要文字區就叫 <main>」,於是在商品列表頁渲染了 12 個 <div class="card"><header>...</header><main>...</main><footer>...</footer></div>。

  • 規範分析:<header> 與 <footer> 確實可以放在每個 <article> 卡片內作為局部標頭與註腳;但 <main> 絕對不能作為局部卡片的內文容器!一份文件最多只能有一個可見的 <main>。卡片內部的正文摘要,直接使用 <div> 或 <p> 包覆即可。

誤區 3:害怕使用 div,把所有純樣式包裝層都換成無標題的 section

部分開發者聽說「要少用 <div>、多用語意標籤」,就把原本用來做內距或背景色的 <div class="bg-gray"><div class="inner-box"> 全部改成 <section class="bg-gray"><section class="inner-box">。

  • 規範分析:根據 WHATWG §4.3.3 (<section>) 官方規範明確指出:> "The section element is not a generic container element. When an element is needed only for styling purposes or as a convenience for scripting, authors are encouraged to use the div element instead."
    > (section 元素不是通用容器。當某個元素僅用於樣式目的或腳本操作便利時,鼓勵開發者改用 div 元素。)

實務 CSS Grid 雙欄佈局範例與選型對應

當我們需要使用 CSS Grid 來實作左右雙欄架構時,務必確保外層容器為 div,內部區塊則嚴格分離 main 與 aside:

/* ✅ 正確的 Grid 語意切版樣式 */
.layout-wrapper {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 320px;
  gap: 2rem;
  max-width: 1280px;
  margin: 0 auto;
  padding: 2rem 1rem;
}

@media (max-width: 768px) {
  .layout-wrapper {
    grid-template-columns: 1fr;
  }
}

四、 實務程式碼對比:從常見誤用結構到標準語意化雙欄佈局

以下我們對比將 <main> 當成萬用容器的錯誤寫法,並提供包含自動化結構檢驗腳本的標準重構範例。

  1. ❌ 常見錯誤寫法(把 main 當全頁外框、將 header/aside/footer 塞入 main、卡片內濫用 main)
<body>
  <!-- ❌ 問題 1:把 <main> 當成全頁 wrapper,導致裡面的 <header> 與 <footer> 失去 banner 與 contentinfo 地標角色 -->
  <main class="app-wrapper">
    <header class="site-header">
      <a href="/">網站品牌</a>
    </header>

    <section class="content-layout">
      <!-- ❌ 問題 2:在文章卡片裡又寫了多個 <main>,違反單頁唯一可見 <main> 規範 -->
      <article class="post-card">
        <header><h2>文章標題一</h2></header>
        <main><p>文章摘要一...</p></main>
      </article>

      <!-- ❌ 問題 3:將跨頁重複的全站側邊欄 <aside> 放在 <main> 內部 -->
      <aside class="global-sidebar">
        <h2>全站熱門標籤</h2>
      </aside>
    </section>

    <footer class="site-footer">
      <p>版權宣告</p>
    </footer>
  </main>
</body>

2. 🟢 生產級標準重構範例(div 專責網格排版 + 四大頂層地標平級分離 + 自動化檢驗腳本)

<!DOCTYPE html>
<html lang="zh-Hant-TW">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>HTML 語意化佈局與 div 排版容器分工程式碼範本</title>
  <style>
    body {
      margin: 0;
      font-family: system-ui, -apple-system, sans-serif;
    }

    /* ✅ 純排版容器使用 div.page-shell,不汙染無障礙地標階層 */
    .page-shell {
      min-height: 100dvh;
      display: grid;
      grid-template-rows: auto 1fr auto;
    }

    .layout-grid {
      width: min(100% - 2rem, 1140px);
      margin-inline: auto;
      display: grid;
      grid-template-columns: 1fr 280px;
      gap: 2rem;
      padding-block: 1.5rem;
    }
  </style>
</head>
<body>
  <div class="page-shell">
    <!-- ✅ 頂層地標 1:全站橫幅 header (不在 main 內,完整保有 role="banner") -->
    <header class="site-header">
      <a href="#main-content" class="skip-link">跳至主要內容</a>
      <nav aria-label="全站主選單">
        <a href="/" aria-current="page">首頁</a>
      </nav>
    </header>

    <!-- ✅ 純雙欄網格容器使用 div.layout-grid -->
    <div class="layout-grid">
      <!-- ✅ 頂層地標 2:全頁唯一主要內容區 main (role="main") -->
      <main id="main-content" tabindex="-1">
        <h1>HTML 語意化標準雙欄結構</h1>

        <!-- 卡片內文使用 div.card-body,不在卡片內濫用 main -->
        <article class="post-card">
          <header>
            <h2>第一篇技術文章標題</h2>
          </header>
          <div class="card-body">
            <p>卡片摘要內容使用 div 或 p 包覆,維持全頁 main 地標唯一性。</p>
          </div>
        </article>
      </main>

      <!-- ✅ 頂層地標 3:與 main 平行的全站側邊欄 aside (role="complementary") -->
      <aside class="global-sidebar" aria-label="全站熱門文章">
        <h2>熱門文章排行</h2>
      </aside>
    </div>

    <!-- ✅ 頂層地標 4:全站頁尾 footer (不在 main 內,完整保有 role="contentinfo") -->
    <footer class="site-footer">
      <p>© 2026 Sean 技術筆記</p>
    </footer>
  </div>

  <script>
    // 🔍 HTML 語意化佈局與地標邊界自動檢測腳本
    function auditMainVsDivLayout() {
      const mainEl = document.querySelector('main:not([hidden])');
      const headerInMain = mainEl ? !!mainEl.querySelector(':scope > header.site-header') : false;
      const footerInMain = mainEl ? !!mainEl.querySelector('footer.site-footer') : false;
      const globalAsideInMain = mainEl ? !!mainEl.querySelector('aside.global-sidebar') : false;

      // 檢查頂層 header 與 footer 是否符合 W3C HTML-AAM 映射為 banner / contentinfo 的祖先條件
      const disallowedLandmarkAncestors = 'article, aside, main, nav, section';
      const topHeader = document.querySelector('header.site-header');
      const topFooter = document.querySelector('footer.site-footer');

      const isBannerValid = topHeader && !topHeader.parentElement.closest(disallowedLandmarkAncestors);
      const isContentinfoValid = topFooter && !topFooter.parentElement.closest(disallowedLandmarkAncestors);

      console.log('=== [HTML 語意化佈局與地標結構檢測報告] ===');
      console.log('1. 全站 <header> 獨立於 <main> 之外 (保有 role="banner"):', !headerInMain && isBannerValid);
      console.log('2. 全站 <footer> 獨立於 <main> 之外 (保有 role="contentinfo"):', !footerInMain && isContentinfoValid);
      console.log('3. 全站側邊欄 <aside> 與 <main> 保持平行獨立:', !globalAsideInMain);
    }

    auditMainVsDivLayout();
  </script>
</body>
</html>

Expected Output (預期 Console 輸出結果)

=== [HTML 語意化佈局與地標結構檢測報告] ===
1. 全站 <header> 獨立於 <main> 之外 (保有 role="banner"): true
2. 全站 <footer> 獨立於 <main> 之外 (保有 role="contentinfo"): true
3. 全站側邊欄 <aside> 與 <main> 保持平行獨立: true

五、 上線前 5 點自我檢驗清單 (Checklist)

在完成頁面切版與提交 Pull Request 前,請逐一核對以下 5 項 HTML 語意化 指標:

  • ☐ 1. 全站 <header> 與 <footer> 未被包進 <main> 內:確認網站頂部導覽列與底部版權頁尾位於 <main> 外部,讓它們在無障礙樹中能正常映射為 role="banner" 與 role="contentinfo"。
  • ☐ 2. 跨頁重複的側邊欄 <aside> 與 <main> 保持平行:若雙欄排版需要共用寬度置中容器,外層已使用 <div class="layout-grid"> 包覆平行的 <main> 與 <aside>,而非把全站側邊欄塞進 <main>。
  • ☐ 3. 純 CSS 排版與樣式外框優先使用 <div>:確認所有僅為了設定 Flexbox / Grid、max-width 置中或背景裝飾而存在的外框皆使用 <div>,沒有濫用無標題的 <section> 或多重 <main>。
  • ☐ 4. 列表卡片內部未使用 <main>:確認 <article> 卡片或 Modal 彈窗內部的正文區塊使用 <div class="card-body">,維持全頁僅有 1 個可見的 <main>。
  • ☐ 5. 通過 Chrome Lighthouse Accessibility 地標檢核:開啟 Chrome DevTools 執行 Lighthouse 無障礙分析,確認未出現 Document does not have a main landmark 或 Banner landmark is contained in another landmark 等結構警告。

參考資料與延伸閱讀

發佈留言