導覽模式:navbar、下拉選單與行動 Drawer 全攻略 | TailwindCSS 完整教學

2026/09/04
導覽模式:navbar、下拉選單與行動 Drawer 全攻略 | TailwindCSS 完整教學

導覽(Navigation) 是使用者在網站裡移動的動線——navbar、下拉選單、麵包屑、Drawer,每一個都在回答「我現在在哪、可以去哪」。這一篇用 TailwindCSS 把桌機水平 navbar、響應式 hamburger 選單、group/peer/details 下拉、行動版 Drawer 側邊抽屜,以及 sticky 固定導覽、麵包屑與 paginationtabs 一次做齊,搭配 v4 語法與少量 JS,兼顧美觀、響應式與無障礙。

前言

導覽(Navigation) 是一個網站的骨架動線。如果說表單是「使用者對網站說話」,那導覽就是「網站告訴使用者可以去哪裡」——它決定了整站的資訊架構如何被觸及,也直接影響使用者能不能在幾秒內找到目標。一個好的導覽,在桌機是一條清爽的水平列,在手機收成一個 hamburger 按鈕;點下去,選單或側滑或展開,始終讓人知道自己身在何處。

先用一個生活化的類比。導覽系統就像一棟百貨公司的樓層指引:大廳入口那排醒目的樓層看板是navbar(一眼看到所有主要分類);走到某個專櫃時牆上「1F > 女裝 > 洋裝」的指示牌是麵包屑(告訴你目前位置的層級);手扶梯旁那面可以拉開的區域導覽圖是下拉選單;而導覽圖太大、平常收在牆邊、需要時才拉出來的那種,就是行動版的 Drawer。同一份「去哪裡」的資訊,在不同情境用不同形式呈現——這正是導覽模式的核心:內容不變,呈現隨螢幕與情境變形

本系列以 TailwindCSS v4 為預設版本。本篇你將學到:

  • 響應式 navbar:桌機水平導覽 + 手機 hamburger 切換,hidden md:flex 的斷點切換心法
  • 下拉與展開選單:用 group(hover)、peer(點擊)、<details>(純 CSS)三種做法各自的適用場景
  • 行動版 Drawer:fixed + translate-x 滑入、遮罩層、Escape 與鎖捲動的 JS 收尾
  • 輔助導覽:sticky 固定列的遮擋問題、麵包屑、pagination 分頁與 tabs 分頁籤

本系列以 TailwindCSS v4 為預設版本,範例皆以 v4 語法示範。凡涉及 v3 差異之處,我會特別標註。

核心概念

導覽模式分類:四種形態各司其職

導覽元件看似五花八門,但可以歸成四大類,理解分類能幫你在對的場景選對工具:

類別代表元件典型用途主要技術
頁面框架導覽Navbar、Sidebar、Bottom Nav承載整站主要連結flexstickyhidden md:flex 斷點切換
內容切換導覽Tabs、Dropdown、Accordion同層內容或子選單切換grouppeer<details>
位置輔助導覽Breadcrumb、Pagination、Step告知位置、翻頁、流程進度flexbefore:contentaria-current
行動導覽Hamburger、Drawer小螢幕收納主導覽translate-xfixed、遮罩 + 少量 JS

這張表值得記的一條線是:同一份導覽資訊,在桌機用「框架導覽」攤開、在手機用「行動導覽」收起。你不會為手機和桌機各寫一套完全不同的連結,而是用 Tailwind 的響應式前綴(md:lg:)控制同一組連結在不同螢幕的呈現形態——桌機 hidden md:flex 顯示水平列,手機顯示 hamburger 觸發的 Drawer。

框架導覽本身也有幾種形態,選哪種取決於應用型態。內容型網站(部落格、行銷頁、電商)幾乎都用頂部 navbar,因為橫向連結少、視覺輕、給內容留最多空間;後台系統、管理面板則常用側邊欄(sidebar),因為功能項目多、需要分組與巢狀,垂直排列比擠在一條橫列上清楚得多——側邊欄用 w-64 固定寬度、flex flex-col 垂直排列,分組用 <p> 小標加 <ul> 群組即可;而純行動 App 風格的介面則偏好底部導覽(bottom nav),用 fixed bottom-0 貼底、md:hidden 只在手機出現,把最常用的 3–5 個入口放在拇指最好按的位置。這三者不是互斥的,一個複雜產品完全可能桌機用側邊欄、手機用底部導覽、行銷頁用頂部 navbar,重點是依情境挑對形態,而非硬套同一種。

響應式導覽策略與斷點

導覽的響應式核心是「哪些東西在哪個螢幕出現」。Tailwind 用斷點前綴精準控制:

螢幕典型模式Tailwind 寫法
手機(< 768px)Hamburger + Drawer,或底部 Bottom Nav桌機連結 hidden、按鈕 md:hidden
平板(768px–1024px)精簡水平 navbarhidden md:flex 顯示核心連結
桌機(> 1024px)完整水平 navbar 或側邊欄lg: 補上次要連結與 CTA

關鍵心法是兩組互斥的顯示切換:桌機導覽區塊用 hidden md:flex(手機藏、桌機顯示),hamburger 按鈕用 md:hidden(手機顯示、桌機藏)。這兩者在同一個斷點 md 交接,達成「小螢幕看到按鈕、大螢幕看到完整列」的無縫切換。

要特別提醒的是斷點的選擇要看內容而非死記md(768px)是最常見的 navbar 切換點,但如果你的桌機導覽連結很多、CTA 又長,768px 可能還塞不下,這時把切換點提到 lg(1024px)更保險;反之連結很少的極簡站台,sm(640px)就能切。判斷準則很單純:在哪個寬度以下,水平列開始擁擠或換行,就在那個斷點收成 hamburger。另外一個實務細節是,行動選單展開的區塊(範例一裡的 #mobile-menu)最好同時掛 md:hidden,避免使用者在手機展開選單後把視窗拉寬到桌機尺寸時,展開選單和桌機水平列同時出現的錯亂畫面。

三種選單觸發機制:group / peer / details

下拉與展開選單是導覽最容易寫錯的地方,先分清楚三種純 CSS(或近乎純 CSS)的機制:

  • group(hover 觸發):在父容器group,子元素用 group-hover:opacity-100 group-hover:visible 依父層 hover 狀態現身。最適合桌機的 hover 下拉選單。缺點是觸控裝置沒有 hover,行動版要另備方案。
  • peer(點擊切換):在前方兄弟元素(通常是隱藏的 <input type="checkbox">)加 peer,後方選單用 peer-checked:block 反應。適合「點一下展開、再點收合」的切換,純 CSS 就能記住開關狀態。
  • <details>/<summary>(語意展開):HTML 原生的可展開容器,<summary> 是點擊觸發器,展開時自動加上 open 屬性,可用 open:group-open: 變體驅動樣式(如箭頭旋轉)。天生具備鍵盤操作與無障礙語意,是最省力、最穩健的展開選單做法。

關鍵術語:aria-current="page" 標記「目前所在頁面」的連結,讓螢幕閱讀器與視覺樣式都能標示當前位置;aria-expanded 表示可展開元素(如 hamburger 按鈕)目前是開是關;aria-controls 把觸發器和它控制的選單 ID 綁定。這三個屬性是導覽無障礙的最小集合,無論用哪種技術實作都不該省略。

還有一個容易被忽略但很重要的角色是語意地標(landmark)。把主導覽包在 <nav aria-label="主要導覽"> 裡,把麵包屑包在 <nav aria-label="麵包屑"> 裡——<nav> 是 HTML 的地標元素,螢幕閱讀器使用者可以用「跳到下一個地標」快速在頁面區塊間穿梭,而 aria-label 讓同一頁多個 <nav> 能被區分。當一頁同時有主導覽、麵包屑、分頁三個 <nav> 時,各自的 aria-label 就是它們的名字。這件事零視覺成本、卻大幅提升輔助技術使用者的效率,是導覽無障礙裡 CP 值最高的一步。

實作範例

理論看完,動手做三個最有代表性的導覽模式:一個響應式 navbar + hamburger、一組下拉選單(group 與 details 兩種寫法),以及行動版 Drawer 側邊抽屜。前提是你已在 globals.css 引入 Tailwind v4(@import "tailwindcss";)。

範例一:響應式 navbar + hamburger 切換

navbar 的骨架是一條 flex 水平列,用 justify-between 把 Logo、連結、操作按鈕分置;桌機連結用 hidden md:flex 只在中大螢幕出現,hamburger 按鈕用 md:hidden 只在手機出現:

<header class="sticky top-0 z-40 border-b border-gray-200 bg-white/80 backdrop-blur-sm">
  <nav aria-label="主要導覽" class="mx-auto flex h-16 max-w-6xl items-center justify-between px-4">
    <!-- 左側:Logo -->
    <a href="/" class="flex shrink-0 items-center gap-2 font-bold text-gray-900">
      <span class="flex size-8 items-center justify-center rounded-lg bg-cyan-600 text-white">B</span>
      BenzHub
    </a>

    <!-- 中間:桌機水平連結(手機隱藏) -->
    <div class="hidden items-center gap-1 md:flex">
      <a href="#" aria-current="page"
         class="rounded-lg bg-cyan-50 px-3 py-2 text-sm font-medium text-cyan-700">首頁</a>
      <a href="#"
         class="rounded-lg px-3 py-2 text-sm font-medium text-gray-600 transition-colors hover:bg-gray-100 hover:text-gray-900">產品</a>
      <a href="#"
         class="rounded-lg px-3 py-2 text-sm font-medium text-gray-600 transition-colors hover:bg-gray-100 hover:text-gray-900">定價</a>
    </div>

    <!-- 右側:桌機 CTA(手機隱藏) -->
    <a href="#"
       class="hidden rounded-lg bg-cyan-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-cyan-700 md:inline-flex">
      免費試用
    </a>

    <!-- 手機 hamburger 按鈕(桌機隱藏) -->
    <button type="button"
            aria-expanded="false" aria-controls="mobile-menu"
            onclick="const m=document.getElementById('mobile-menu');
                     const open=m.classList.toggle('hidden');
                     this.setAttribute('aria-expanded', String(!open));"
            class="rounded-lg p-2 text-gray-600 transition-colors hover:bg-gray-100 md:hidden">
      <span class="sr-only">開啟選單</span>
      <svg class="size-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
      </svg>
    </button>
  </nav>

  <!-- 手機展開選單(桌機永遠隱藏) -->
  <div id="mobile-menu" class="hidden border-t border-gray-200 px-4 py-3 md:hidden">
    <a href="#" aria-current="page"
       class="block rounded-lg bg-cyan-50 px-3 py-2 text-sm font-medium text-cyan-700">首頁</a>
    <a href="#" class="block rounded-lg px-3 py-2 text-sm text-gray-700 hover:bg-gray-100">產品</a>
    <a href="#" class="block rounded-lg px-3 py-2 text-sm text-gray-700 hover:bg-gray-100">定價</a>
  </div>
</header>

幾個關鍵細節值得拆解。斷點互斥切換是這段的核心:桌機連結區塊 hidden md:flex(小螢幕藏、md 以上以 flex 顯示),hamburger 按鈕 md:hidden(小螢幕顯示、md 以上藏),兩者剛好在 md 斷點無縫交接。hamburger 按鈕同步 aria-expanded:點擊時不只切換選單的 hidden,還把 aria-expandedfalse 改成當前狀態,螢幕閱讀器才知道選單是開是關——這一步最常被漏掉。navbar 用 sticky top-0 z-40 讓它捲動時固定在頂端,z-40 確保它蓋在內容之上,bg-white/80 backdrop-blur-sm 則做出半透明毛玻璃效果。

範例二:下拉選單(group hover 與 details 兩種寫法)

先看桌機 hover 下拉,用 group:在父容器加 group,面板預設 invisible opacity-0,父層 hover 時用 group-hover: 現身,再加 group-focus-within: 讓鍵盤 Tab 進去也能展開:

<div class="group relative inline-block">
  <button type="button"
          class="flex items-center gap-1 rounded-lg px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100">
    產品
    <svg class="size-4 text-gray-400 transition-transform group-hover:rotate-180"
         fill="none" stroke="currentColor" viewBox="0 0 24 24">
      <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
    </svg>
  </button>

  <!-- 下拉面板:hover 或內部聚焦時現身 -->
  <div class="invisible absolute left-0 top-full z-50 mt-1 w-56 rounded-xl border border-gray-200 bg-white p-1 opacity-0 shadow-lg transition-all
              group-hover:visible group-hover:opacity-100
              group-focus-within:visible group-focus-within:opacity-100">
    <a href="#" class="block rounded-lg px-3 py-2 text-sm text-gray-700 hover:bg-gray-100">產品總覽</a>
    <a href="#" class="block rounded-lg px-3 py-2 text-sm text-gray-700 hover:bg-gray-100">功能列表</a>
    <a href="#" class="block rounded-lg px-3 py-2 text-sm text-gray-700 hover:bg-gray-100">價格方案</a>
  </div>
</div>

這裡用 invisible opacity-0 而非 hidden 來隱藏面板,是為了讓 transition-all 的淡入動畫有作用(hidden 無法過渡)。group-focus-within: 這一組是別忘的無障礙補強:純用 group-hover: 的話,鍵盤使用者 Tab 進面板時它是隱形的,加上 group-focus-within: 才能讓鍵盤操作也展開。

但 hover 下拉在觸控裝置上沒有 hover,行動版該怎麼辦?最穩健的答案是改用 <details>,它天生支援點擊觸發與鍵盤操作,不挑裝置:

<details class="group relative">
  <summary
    class="flex cursor-pointer list-none items-center gap-1 rounded-lg px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100">
    產品
    <!-- 展開時箭頭旋轉:用 group-open: 偵測 details 的 open 狀態 -->
    <svg class="size-4 text-gray-400 transition-transform group-open:rotate-180"
         fill="none" stroke="currentColor" viewBox="0 0 24 24">
      <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
    </svg>
  </summary>

  <div class="mt-1 w-56 rounded-xl border border-gray-200 bg-white p-1 shadow-lg">
    <a href="#" class="block rounded-lg px-3 py-2 text-sm text-gray-700 hover:bg-gray-100">產品總覽</a>
    <a href="#" class="block rounded-lg px-3 py-2 text-sm text-gray-700 hover:bg-gray-100">功能列表</a>
  </div>
</details>

<details> 版的巧妙有三:<summary>list-none(搭配 cursor-pointer)收掉瀏覽器預設的三角箭頭,改用自訂 SVG;group-open:rotate-180 讓箭頭在展開時旋轉,group-open: 對應 <details> 展開後自動加上的 open 屬性;整段零 JavaScript,點擊展開、再點收合、Enter/Space 鍵操作全部原生支援。這也呼應了上一篇《表單模式》的原則——能交給原生與純 CSS 的,就別自己刻

範例三:行動版 Drawer 側邊抽屜

Drawer 是需求最完整的行動導覽:它從畫面外滑入、蓋一層遮罩、點遮罩或按 Escape 關閉、開啟時鎖住背景捲動。這些行為超出純 CSS 能力,得用少量 JavaScript 收尾。核心 Tailwind 技巧是 fixed inset-y-0 left-0 貼左全高 + -translate-x-full(初始推到畫面外)/ translate-x-0(滑入歸位)的位移切換:

<!-- 觸發按鈕 -->
<button type="button" onclick="openDrawer()"
        aria-controls="drawer" aria-expanded="false"
        class="rounded-lg p-2 text-gray-600 hover:bg-gray-100 md:hidden">
  <span class="sr-only">開啟選單</span>
  <svg class="size-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
  </svg>
</button>

<!-- 遮罩層:z-index 要比 Drawer 低、比 navbar 高 -->
<div id="drawer-overlay" onclick="closeDrawer()"
     class="fixed inset-0 z-40 hidden bg-black/50 transition-opacity"></div>

<!-- Drawer 主體 -->
<aside id="drawer" aria-label="行動導覽選單"
       class="fixed inset-y-0 left-0 z-50 flex w-72 -translate-x-full flex-col bg-white shadow-xl transition-transform duration-300 ease-in-out">
  <div class="flex h-16 shrink-0 items-center justify-between border-b border-gray-200 px-4">
    <span class="font-bold text-gray-900">BenzHub</span>
    <button type="button" onclick="closeDrawer()"
            class="rounded-lg p-2 text-gray-500 hover:bg-gray-100">
      <span class="sr-only">關閉選單</span>
      <svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
      </svg>
    </button>
  </div>
  <nav class="flex-1 space-y-1 overflow-y-auto p-3">
    <a href="#" aria-current="page"
       class="block rounded-lg bg-cyan-50 px-3 py-2.5 text-sm font-medium text-cyan-700">首頁</a>
    <a href="#" class="block rounded-lg px-3 py-2.5 text-sm text-gray-700 hover:bg-gray-100">產品</a>
    <a href="#" class="block rounded-lg px-3 py-2.5 text-sm text-gray-700 hover:bg-gray-100">定價</a>
  </nav>
</aside>

<script>
  const drawer  = document.getElementById('drawer');
  const overlay = document.getElementById('drawer-overlay');

  function openDrawer() {
    drawer.classList.replace('-translate-x-full', 'translate-x-0'); // 滑入
    overlay.classList.remove('hidden');                             // 顯示遮罩
    document.body.classList.add('overflow-hidden');                 // 鎖背景捲動
  }
  function closeDrawer() {
    drawer.classList.replace('translate-x-0', '-translate-x-full'); // 滑出
    overlay.classList.add('hidden');
    document.body.classList.remove('overflow-hidden');
  }
  // 按 Escape 關閉
  document.addEventListener('keydown', (e) => {
    if (e.key === 'Escape') closeDrawer();
  });
</script>

拆解 Drawer 的四個關鍵。位移動畫是靈魂:-translate-x-full 讓 Drawer 初始躲在畫面左外側,transition-transform duration-300 加上 JS 切換到 translate-x-0 就得到平滑滑入;比起切換 hidden,位移動畫的體驗好得多。z-index 的三層堆疊要記牢:內容最底、遮罩 z-40、Drawer z-50——遮罩必須蓋住內容但露出 Drawer。鎖背景捲動document.bodyoverflow-hidden,避免 Drawer 開著時背景還能滑動的尷尬。Escape 關閉是鍵盤無障礙的基本盤,keydown 監聽 Escape 一行就補上。

輔助導覽:sticky、麵包屑、pagination 與 tabs

主導覽之外,幾個輔助元件也常用。先看麵包屑,用 <ol>aria-label 語意化,分隔符用 before:content 純 CSS 產生,不佔語意:

<nav aria-label="麵包屑">
  <ol class="flex items-center text-sm">
    <li><a href="/" class="text-gray-500 hover:text-gray-900">首頁</a></li>
    <li class="before:mx-2 before:text-gray-300 before:content-['/']">
      <a href="/tailwindcss" class="text-gray-500 hover:text-gray-900">TailwindCSS</a>
    </li>
    <li class="before:mx-2 before:text-gray-300 before:content-['/']">
      <span aria-current="page" class="font-medium text-gray-900">導覽模式</span>
    </li>
  </ol>
</nav>

pagination 分頁flex 排一列頁碼,當前頁用 aria-current="page" 標記並填色:

<nav aria-label="分頁導覽">
  <ul class="flex items-center gap-1">
    <li><a href="#" class="rounded-lg border border-gray-300 px-3 py-2 text-sm text-gray-600 hover:bg-gray-50">上一頁</a></li>
    <li><a href="#" class="flex size-9 items-center justify-center rounded-lg border border-gray-300 text-sm text-gray-600 hover:bg-gray-50">1</a></li>
    <li><a href="#" aria-current="page" class="flex size-9 items-center justify-center rounded-lg bg-cyan-600 text-sm font-medium text-white">2</a></li>
    <li><a href="#" class="flex size-9 items-center justify-center rounded-lg border border-gray-300 text-sm text-gray-600 hover:bg-gray-50">3</a></li>
    <li><a href="#" class="rounded-lg border border-gray-300 px-3 py-2 text-sm text-gray-600 hover:bg-gray-50">下一頁</a></li>
  </ul>
</nav>

tabs 分頁籤做同層內容切換,底線式最常見:當前頁籤用 border-b-2 border-cyan-600,面板用 role="tabpanel" 對應(切換邏輯需少量 JS,此處聚焦樣式):

<div class="border-b border-gray-200">
  <div class="-mb-px flex gap-4" role="tablist" aria-label="內容分類">
    <button role="tab" aria-selected="true"
            class="border-b-2 border-cyan-600 px-1 py-3 text-sm font-medium text-cyan-600">概覽</button>
    <button role="tab" aria-selected="false"
            class="border-b-2 border-transparent px-1 py-3 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700">功能</button>
    <button role="tab" aria-selected="false"
            class="border-b-2 border-transparent px-1 py-3 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700">定價</button>
  </div>
</div>

底線式 tabs 的小技巧是外層容器 border-b(整條底線)搭配內層 -mb-px(把頁籤往下推一像素),讓當前頁籤的 border-b-2 剛好蓋在容器底線上,對齊得乾淨俐落。

順帶一提膠囊式 tabs(pill tabs) 是另一種常見變體,把整組頁籤放進一個 bg-gray-100 rounded-xl p-1 的容器,當前頁籤用 bg-white shadow-sm rounded-lg 做出「浮起」感,適合放在卡片內或工具列。兩種 tabs 的取捨很簡單:底線式更輕、適合大區塊內容切換(像文章的概覽/功能/定價),膠囊式更聚焦、適合小範圍的篩選切換(像「全部/進行中/已完成」)。而 tabs 與下拉選單的差別也值得釐清——tabs 是「同層平行內容」的切換,選單是「跳往不同頁面」的導覽,兩者語意不同,別把該用 tabs 的地方做成一堆連結。

常見錯誤與最佳實踐

坑一:hamburger 選單只切 hidden,不同步 aria-expanded

最常見的無障礙疏漏,就是點 hamburger 只做 classList.toggle('hidden'),卻沒更新按鈕的 aria-expanded。這樣視覺上選單開了,但螢幕閱讀器仍以為它是關的,鍵盤使用者無從得知狀態:

<!-- ❌ 只切選單顯示,按鈕狀態沒同步 -->
<button aria-expanded="false"
        onclick="menu.classList.toggle('hidden')">選單</button>

正確做法:切換顯示的同時同步 aria-expanded,並用 aria-controls 綁定選單 ID(範例一即如此)。純 CSS 版若改用 <details>,則能天然免除這個問題——<details> 的展開狀態瀏覽器會自動反映給輔助技術。

坑二:sticky/fixed 導覽遮住錨點內容

sticky top-0 的 navbar 很好用,但點錨點連結跳到某段落時,段落頂端會被固定的 navbar 蓋住,讀者看到的是被切掉一截的標題:

<!-- ❌ 點 #section 跳過去,標題被 h-16 的 navbar 遮住 -->
<h2 id="section">章節標題</h2>

正確做法:給錨點目標加 scroll-mt-*(對應 CSS scroll-margin-top),預留 navbar 的高度。navbar 是 h-16,目標就加 scroll-mt-16,甚至可以全域套用:

<!-- ✅ 捲動定位時預留 navbar 高度,標題不被遮 -->
<h2 id="section" class="scroll-mt-16">章節標題</h2>

另外別忘了 sticky 的兩個前提:它需要一個可捲動的父容器,且父容器不能有 overflow-hidden,否則 sticky 會悄悄失效——這是很多人 debug 半天找不到原因的隱形坑。

坑三:hover 下拉在手機上完全無法使用

group-hover: 做的桌機下拉在觸控裝置上沒有 hover 事件,手機使用者點下去毫無反應,等於選單消失:

<!-- ❌ 只靠 hover:觸控裝置點不開 -->
<div class="group">
  <button>產品</button>
  <div class="invisible group-hover:visible">...選單...</div>
</div>

正確做法:hover 下拉限用於桌機(hidden md:block),行動版改用 <details> 點擊展開或收進 Drawer;或直接全站統一用 <details>,一套邏輯桌機手機通吃。原則是任何導覽都必須在觸控裝置上可操作,hover 只能是桌機的增強,不能是唯一入口。

坑四:移除 outline 卻不補聚焦樣式

導覽全是可聚焦的連結與按鈕,為了美觀寫 focus:outline-none 卻不補替代樣式,鍵盤使用者 Tab 過導覽時會完全看不出焦點在哪:

<!-- ❌ 拿掉 outline 又不補,鍵盤導覽迷路 -->
<a href="#" class="focus:outline-none">連結</a>

正確做法:移除預設 outline 的同時,用 focus-visible: 補一個清晰的聚焦環,只在鍵盤聚焦時顯示,不干擾滑鼠點擊:

<!-- ✅ 補上 focus-visible 聚焦環 -->
<a href="#"
   class="rounded-lg focus:outline-none focus-visible:ring-2 focus-visible:ring-cyan-500 focus-visible:ring-offset-2">連結</a>

最佳實踐小結

  • 斷點互斥切換:桌機導覽 hidden md:flex、hamburger md:hidden,在同一斷點無縫交接。
  • 選單技術選型:hover 下拉用 group(限桌機)、點擊切換用 peer、語意展開用 <details>(最穩健)。
  • Drawer 用少量 JS:translate-x 滑入 + 遮罩 + Escape + 鎖捲動,無障礙屬性同步不可省。
  • sticky 防遮擋:錨點目標加 scroll-mt-16,父容器別用 overflow-hidden
  • 無障礙最小集:aria-currentaria-expandedaria-controls,加上聚焦樣式必補。

小結

這是 TailwindCSS 完整教學 系列的第三十五篇。上一篇《表單模式》,我們把最考驗一致性的輸入框、驗證與自訂控制項一路樣式化;這一篇,我們把視線從「單一頁面的輸入」拉高到「整站的移動動線」,看導覽如何讓使用者在頁面間穿梭時始終知道自己在哪、能去哪。回顧幾個重點:

  • 四種導覽形態:頁面框架(navbar/sidebar)、內容切換(tabs/dropdown)、位置輔助(breadcrumb/pagination)、行動導覽(hamburger/drawer),同一份資訊隨螢幕變形。
  • 響應式切換:hidden md:flexmd:hidden 兩組互斥顯示,是桌機水平列與手機 hamburger 無縫交接的核心。
  • 選單三機制:group(hover)、peer(點擊)、<details>(語意展開)各有適用場景,能純 CSS 就別寫 JS,<details> 最穩健。
  • Drawer 與輔助導覽:translate-x 滑入的 Drawer 需 JS 補齊遮罩、Escape 與鎖捲動;麵包屑、pagination、tabs 用 flexaria-current 就能做得語意完整。
  • 無障礙:aria-current/aria-expanded/aria-controls 三件套、sticky 用 scroll-mt-* 防遮擋、移除 outline 必補 focus-visible: 聚焦環。

把導覽做好,使用者才不會在你的網站裡迷路。接下來,我們要把焦點放到「網站如何回應使用者的操作」——下一篇《回饋模式》,將帶你用 TailwindCSS 打造 alert、toast、loading、skeleton 與 modal,看介面如何在使用者每一次點擊後給出清楚、即時又不打擾的回饋。

BenZ Software Developer

熱愛技術的軟體開發者,在這裡分享程式開發經驗與學習筆記。

本週主打

AI 自動化入門包

你每天手動在做的那些煩事,其實 AI 可以自己跑。這份給你 10 個照著做就會的自動化工作流 + 50 個複製即用的提示詞,不用會寫程式。

看看這個產品 →