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

HTML, 新手 Jamp Holes

HTML nav 標籤與一般 div 的本質差異:為什麼語意化標籤對新手至關重要?

HTML 標籤與一般 的本質差異:為什麼語意化標籤對新手至關重要? 特色圖片

💡 文章重點摘要:在日常專案開發與 Code Review 中,時常看到新手甚至部分中階工程師患上嚴重的「<div> 症候群(Divitis)」——忽略 HTML5 語意化標籤的價值,從頂部導覽列、側邊目錄到底部分頁器,清一色用 <div class="nav-bar"> 堆疊到底。雖然在一般螢幕上看長得一模一樣,但在搜尋引擎爬蟲與視障使用者的無障礙樹(Accessibility Tree)眼中,兩者卻有天壤之別!本文將帶你從底層地標角色(Landmark Roles)映射機制切入,掌握 WHATWG 官方對 <nav> 的使用邊界決策樹、aria-current="page" 狀態綁定,以及 Safari 下 list-style: none 抹殺列表語意等 4 大生產環境避坑實戰!


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

如果你想立刻將網站的主導覽列與麵包屑升級為符合 W3C 與 SEO 標準的 <nav> 語意化標籤結構(含多導覽列 aria-label 區分、aria-current="page" 當前頁標記與 Safari 列表語意防禦),可直接複製下方範本:

<!-- 1. 全站主選單:同頁有多個 <nav> 時,必須以 aria-label 標註唯一名稱 -->
<nav aria-label="全站主選單">
  <!-- Safari 防坑:當 CSS 設定 list-style: none 時,顯式加上 role="list" 保住清單語意 -->
  <ul class="nav-list" role="list">
    <li><a href="/" aria-current="page">首頁</a></li>
    <li><a href="/tutorials">技術文章</a></li>
    <li><a href="/about">關於站長</a></li>
  </ul>
</nav>

<!-- 2. 麵包屑導覽:使用有序列表 <ol> 表達層級順序 -->
<nav aria-label="麵包屑路徑">
  <ol class="breadcrumb-list" role="list">
    <li><a href="/">首頁</a></li>
    <li><a href="/html">HTML 專欄</a></li>
    <li><span aria-current="page">語意化標籤指南</span></li>
  </ol>
</nav>

💡 3 秒修改重點:

  1. 一個頁面若有 2 個以上的 <nav>,務必加上 aria-label(不要包含「導覽」二字以免螢幕閱讀器重複播報,例如寫 aria-label="主選單")。
  2. 當前所在頁面的連結請加上 aria-current="page",讓螢幕閱讀器直接播報「目前頁面」。

一、 為什麼視覺長一樣,底層卻天差地遠?DOM 樹與無障礙樹雙軌揭秘

許多初入前端領域的新手常問:「<nav> 跟 <div> 在瀏覽器預設樣式裡都只是 display: block 的區塊元素,為什麼不能直接寫 <div class="nav"> 就好?」

答案在於:現代瀏覽器在解析 HTML 時,除了建立供 CSS 渲染畫面使用的 DOM 樹(Document Object Model Tree)之外,還會在底層同步建立一棵專門提供給輔助科技(如螢幕閱讀器 NVDA、JAWS、VoiceOver)與搜尋引擎理解頁面結構的 無障礙樹(Accessibility Tree / AOM)!

  • <div> 的本質:純粹的通用排版容器(Generic Container),在 WAI-ARIA 規範中對應 role="generic",在無障礙樹與爬蟲眼中完全沒有任何結構地標意義。
  • <nav> 的本質:HTML5 區段語意化標籤(Sectioning Content),瀏覽器出廠即自動賦予它隱含的 role="navigation" 地標角色(Landmark Role)。
為什麼視覺長一樣,底層卻天差地遠?DOM 樹與無障礙樹雙軌揭秘 視覺化架構流程圖
圖:為什麼視覺長一樣,底層卻天差地遠?DOM 樹與無障礙樹雙軌揭秘 視覺化架構流程圖

當視障使用者進入你的網站時,他們不會從頭到尾聽完幾百個字,而是直接按下螢幕閱讀器的地標快捷鍵(例如在 NVDA / JAWS 按下 D 鍵切換不同 Landmark 地標,或按下 N 鍵跳過整串導覽連結直達非連結內文;在 macOS VoiceOver 則透過 Rotor 轉盤切換地標)。如果你的導覽列是用 <div> 寫的,這群使用者將完全找不到網站選單,也無法一鍵跳過冗長的頂部連結!


二、 5 秒快速索引:nav vs div 與核心地標標籤速查表

在切版選型時,請直接對照這張 HTML5 結構語意標籤與 <div> 的職責對照表:

標籤名稱隱含 ARIA 角色 (Implicit Role)單頁建議數量核心語意職責與最佳使用時機生產環境關鍵注意事項
<nav>role="navigation"1 ~ 多個包覆主要導覽連結群組(主選單、麵包屑、文章目錄 TOC、分頁器)。若同頁出現 2 個以上 <nav>,必須加上 aria-label 或 aria-labelledby 區分名稱。
<div>role="generic"不限零語意的純視覺佈局、Flex/Grid 容器、樣式包裝器(Wrapper)。找不到任何符合語意的原生標籤時,才退而求其次使用 <div>。
<header>role="banner"
(位於 <body> 頂層時)
全頁頂層 1 個
(區段內不限)
網站頂部橫幅(含 Logo、搜尋框、主選單)或 <article> 的標題資訊區。若包在 <article> 或 <section> 內,會失去全站 banner 地標身分,變為一般區段標頭。
<main>role="main"全頁唯一 1 個 (顯示狀態下)當前頁面獨一無二的核心主內容區(排除全站共用的頂欄、側欄與頁尾)。搭配頂部「跳至主要內容(Skip Link)」錨點(href="#main-content"),大幅提升鍵盤體驗。
<aside>role="complementary"1 ~ 多個與主內容間接相關的附屬區塊(側邊推薦欄、廣告欄、作者簡介卡)。切勿把全站主導覽列直接塞在 <aside> 而不包 <nav>。
<footer>role="contentinfo"
(位於 <body> 頂層時)
全頁頂層 1 個
(區段內不限)
頁尾版權宣告、隱私權條款、聯絡資訊與次要網站地圖(Sitemap)連結。頁尾簡短的「版權與服務條款」連結直接放 <footer> 即可,不需額外硬包 <nav>。

三、 什麼時候該用 nav?WHATWG 官方邊界決策樹

新手最常犯的兩個極端錯誤,一是「全站一個 <nav> 都不用」,二是**「矯枉過正,只要看到 <a> 超連結就通通拿 <nav> 包起來」**!

根據 WHATWG HTML Living Standard 4.3.4 規範,<nav> 僅適用於「主要導覽區塊(Major Navigation Blocks)」。為了讓你在切版時 3 秒內做出正確判斷,請遵循以下決策樹:

什麼時候該用 ?WHATWG 官方邊界決策樹 視覺化架構流程圖
圖:什麼時候該用 ?WHATWG 官方邊界決策樹 視覺化架構流程圖

四、 生產級實戰:重構「萬年 div 導覽列」與 aria-current 最佳實踐

讓我們將一個典型的「非語意化 div 頂部導覽列」,升級為符合現代 WCAG 無障礙標準與 SEO 規範的生產級程式碼。

1. ❌ 舊版反模式(Divitis 反面教材)

<!-- ❌ 錯誤示範:全用 div 堆疊、靠 .active class 變色卻無語意、缺少跳過導覽連結 -->
<div class="header">
  <div class="logo"><a href="/">TechBlog</a></div>
  <div class="nav-bar">
    <div class="nav-item active"><a href="/articles">技術文章</a></div>
    <div class="nav-item"><a href="/tutorials">實戰教學</a></div>
    <div class="nav-item"><a href="/about">關於站長</a></div>
  </div>
</div>
<div class="main-body">
  <h1>最新技術文章</h1>
</div>

這段程式碼在生產環境的 3 大缺陷:

  1. 鍵盤使用者每次換頁,都必須狂按數十次 Tab 鍵穿過頂部選單,才能抵達正文的按鈕。
  2. 視障使用者完全不知道目前所在的頁面是「技術文章」,因為 .active 只改變了視覺顏色,無障礙樹對此一無所知。
  3. 爬蟲與閱讀器無法識別 .nav-bar 是導覽地標。

2. 🟢 現代語意化生產級重構(含 Skip Link + aria-current="page")

<!-- 🟢 最佳實踐:結合 Skip Link、header、nav、role="list" 與 aria-current -->
<header class="site-header">
  <!-- 1. 無障礙逃生門 (Skip Link):平時隱藏,按下 Tab 鍵時浮現,一鍵跳過導覽列直達正文 -->
  <a href="#main-content" class="skip-link">跳至主要內容</a>

  <div class="brand-logo">
    <a href="/">TechBlog</a>
  </div>

  <!-- 2. 明確標註 aria-label,與頁面底部的分頁導覽或麵包屑做出區隔 -->
  <nav class="primary-nav" aria-label="全站主選單">
    <!-- 3. 顯式加上 role="list",防止 Safari 因 CSS list-style: none 抹殺列表語意 -->
    <ul class="nav-list" role="list">
      <li>
        <!-- 4. 使用 aria-current="page" 宣告當前頁面,並直接作為 CSS 樣式選擇器! -->
        <a href="/articles" aria-current="page">技術文章</a>
      </li>
      <li><a href="/tutorials">實戰教學</a></li>
      <li><a href="/about">關於站長</a></li>
    </ul>
  </nav>
</header>

<!-- 5. 對應 Skip Link 的唯一 main 地標容器,設定 tabindex="-1" 確保程式化錨點聚焦穩定 -->
<main id="main-content" tabindex="-1">
  <h1>最新技術文章</h1>
</main>
/* 1. Skip Link 樣式:預設移出畫面外,僅在鍵盤 Tab 聚焦 (:focus-visible) 時優雅浮現 */
.skip-link {
  position: absolute;
  top: -100px;
  left: 16px;
  z-index: 9999;
  padding: 8px 16px;
  background-color: #0f172a;
  color: #ffffff;
  font-weight: 600;
  border-radius: 6px;
  text-decoration: none;
  transition: top 0.2s ease;
}

.skip-link:focus-visible {
  top: 12px;
  outline: 3px solid #38bdf8;
  outline-offset: 2px;
}

/* 2. 導覽清單重置:配合 HTML 上的 role="list" 確保跨瀏覽器語意不丟失 */
.nav-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  gap: 24px;
}

/* 3. 狀態驅動 CSS:直接綁定 [aria-current="page"],徹底淘汰無語意的 .active class! */
.nav-list a {
  color: #475569;
  text-decoration: none;
  font-weight: 500;
  padding: 6px 0;
}

.nav-list a[aria-current="page"] {
  color: #0284c7;
  font-weight: 700;
  border-bottom: 2px solid #0284c7;
}

Expected Output (預期執行結果)

  1. 鍵盤導航體驗:進入頁面按一下 Tab 鍵,左上角立即浮現深色「跳至主要內容」按鈕;按下 Enter 後,焦點直接跳過整個主選單,精準落入 <main id="main-content">。
  2. 螢幕閱讀器播報:進入選單時會清晰播報:「全站主選單,導覽地標,清單,共 3 個項目」;聚焦於第一個連結時會播報:「技術文章,目前頁面,連結」。
  3. CSS 與狀態 100% 同步:永遠不會再發生「改了 .active 卻忘記改無障礙屬性」的脫節問題,因為視覺高亮直接由 [aria-current="page"] 驅動!

五、 生產環境常見的 4 大 nav 踩坑與排錯指南 (Pitfalls & Troubleshooting)

地雷 1:同頁多個 nav 忘記加 aria-label,引發無障礙地標「撞名」

  • ❌ 錯誤現象:一個部落格文章頁同時有「頂部主選單 <nav>」、「麵包屑 <nav>」、「文章右側目錄 <nav>」與「底部上一篇/下一篇分頁 <nav>」,全部都只寫光禿禿的 <nav>。
  • 💥 為什麼會壞:當視障使用者打開螢幕閱讀器的地標清單(Landmark List)時,會看到畫面上列出四個一模一樣的「導覽 (navigation)、導覽 (navigation)、導覽 (navigation)、導覽 (navigation)」,完全無法分辨哪個才是麵包屑或主選單!
  • 解決思路:只要同頁有超過一個 <nav>,每一個都必須透過 aria-label(或指向內部可見標題 ID 的 aria-labelledby)賦予唯一且具描述性的名稱。
    • ⚠️ 重要細節:aria-label 的內容切勿包含「導覽」或「Navigation」字眼!例如不要寫 aria-label="主選單導覽",否則螢幕閱讀器會重複唸出「主選單導覽,導覽」。直接寫 aria-label="主選單"、aria-label="麵包屑路徑"、aria-label="文章章節分頁" 即可。

地雷 2:在 CSS 寫了 list-style: none,結果 Safari 把 ul 的列表語意吃掉了!

  • ❌ 踩坑現象:在 Chrome 和 Firefox 測試 <nav><ul><li>...</li></ul></nav> 都很正常,為什麼在 macOS / iOS Safari 搭配 VoiceOver 測試時,有時卻聽不到「清單,共 N 個項目」的提示?
  • 💥 為什麼會壞:這是 Apple WebKit 引擎一個極為著名的設計決策。由於早年太多開發者濫用 <ul> 加上 list-style: none 來做一般排版,WebKit 決定:只要 <ul> 或 <ol> 在 CSS 中被設定了 list-style: none(或 list-style-type: none),WebKit 就會在無障礙樹中移除它的隱含 role="list" 語意!
  • 解決思路:凡是在需要清除小圓點的導覽列表或商品列表上,養成習慣在 HTML 顯式補上 <ul role="list">,即可強制在 Safari / VoiceOver 下完整保留列表語意。

地雷 3:迷信「nav 裡面只能放 ul」,不敢放 ol 或 p

  • ❌ 迷思:許多教學文章宣稱「<nav> 裡面規定只能寫 <ul><li>」。
  • 💥 真相還原:根據 WHATWG 官方規範,<nav> 內部的結構應該依據導覽連結本身的順序語意決定:
    1. 無序並列的選單(如首頁、產品、關於我們):使用無序列表 <ul>。
    2. 具備嚴格先後順序的導覽(如「麵包屑 Breadcrumbs:首頁 > 分類 > 文章」或「多步驟結帳流程導覽」):必須使用有序列表 <ol>!
    3. 簡短的句子型導覽:WHATWG 明確指出,在 <nav> 內使用 <p> 段落包覆導覽連結也是完全合法的標準寫法。

地雷 4:把搜尋框 form 或行動版漢堡按鈕塞錯位置

  • ❌ 錯誤寫法:把整個網站的 <header> 內容(包含品牌 Logo、全站搜尋框、購物車彈窗按鈕)全部一股腦包進同一個 <nav> 裡面。
  • 解決思路:<header> 才是頂部橫幅的總容器。Logo 與搜尋表單應直接放在 <header> 內,與 <nav> 保持兄弟節點(Sibling)關係;唯有控制選單展開/收合的漢堡按鈕(<button aria-expanded="false" aria-controls="nav-menu">),應放在 <nav> 內部作為第一個子元素,讓螢幕閱讀器使用者進入 <nav> 地標時能第一時間觸發按鈕展開選單!

六、 工程師上線前必備:HTML nav 與語意化導覽檢驗清單 (Checklist)

在切版完成或提交 Code Review 前,請對照以下 5 點清單進行自我查核:

  • ☐ 地標精準度檢核:主選單、麵包屑、文章目錄(TOC)與分頁器皆已採用 <nav> 包覆;頁尾簡短條款與文章內嵌連結未濫用 <nav>。
  • ☐ 多 <nav> 唯一命名:頁面中若存在多個 <nav>,皆已加上不重複的 aria-label(且字串末尾未贅加「導覽 / Navigation」字詞)。
  • ☐ 當前頁語意綁定:目前所在頁面的導覽連結已掛載 aria-current="page"(麵包屑最後一項亦同),並以 [aria-current="page"] 驅動高亮樣式。
  • ☐ 有序與無序列表區分 & WebKit 防護:一般選單使用 <ul>、麵包屑與分頁順序使用 <ol>;且凡設定 list-style: none 的列表皆已顯式補上 role="list"。
  • ☐ 鍵盤逃生門(Skip Link):<header> 頂部第一位已配置指向 <main id="main-content" tabindex="-1"> 的「跳至主要內容」隱藏連結,且按 Tab 鍵時具備清晰的 :focus-visible 視覺焦點框。

七、 參考資料與延伸閱讀

發表迴響