建立側邊導覽列元件

基礎總覽:如何建構回應式滑出式側邊導覽

在這篇文章中,我想分享如何為網頁製作 Sidenav 元件原型,這個元件具有回應性、有狀態、支援鍵盤導覽、可搭配或不搭配 JavaScript 運作,且適用於各種瀏覽器。試用示範模式

如果比較喜歡看影片,可以觀看這篇貼文的 YouTube 版本:

總覽

建構回應式導覽系統並不容易,部分使用者會使用鍵盤,部分使用者會使用功能強大的桌機,部分使用者則會透過小型行動裝置造訪。所有訪客都應該能開啟及關閉選單。

Desktop to mobile responsive layout demo
Light and dark theme down on iOS and Android

網站策略

在探索這個元件時,我很高興能結合幾項重要的網頁平台功能:

  1. CSS :target
  2. CSS 格線
  3. CSS 變形
  4. 可視區域和使用者偏好設定的 CSS 媒體查詢
  5. focus UX 強化功能的 JS

我的解決方案有一個側欄,且只會在 540px 以下的「行動裝置」可視區域切換。 540px 將做為分界點,用於在行動裝置互動式版面配置和靜態電腦版面配置之間切換。

CSS :target 虛擬類別

其中一個 <a> 連結會將網址雜湊設為 #sidenav-open,另一個則設為空白 ('')。 最後,元素會使用 id 來比對雜湊:

<a href="#sidenav-open" id="sidenav-button" title="Open Menu" aria-label="Open Menu">

<a href="#" id="sidenav-close" title="Close Menu" aria-label="Close Menu"></a>

<aside id="sidenav-open">
  …
</aside>

按一下這些連結會變更網頁網址的雜湊狀態,然後我會使用虛擬類別顯示及隱藏側邊導覽列:

@media (max-width: 540px) {
  #sidenav-open {
    visibility: hidden;
  }

  #sidenav-open:target {
    visibility: visible;
  }
}

CSS 格線

過去我只使用絕對或固定位置的側邊導覽列版面配置和元件。不過,格線的 grid-area 語法可讓我們將多個元素指派給同一列或同一欄。

堆疊

主要版面配置元素 #sidenav-container 是格線,會建立 1 個資料列和 2 個資料欄,其中 1 個資料列和 1 個資料欄分別命名為 stack。空間受限時,CSS 會將所有 <main> 元素的子項指派給相同的格線名稱,將所有元素放置在同一空間中,建立堆疊。

#sidenav-container {
  display: grid;
  grid: [stack] 1fr / min-content [stack] 1fr;
  min-height: 100vh;
}

@media (max-width: 540px) {
  #sidenav-container > * {
    grid-area: stack;
  }
}

<aside> 是包含側邊導覽的動畫元素。包含 2 個子項:名為 [nav] 的導覽容器 <nav>,以及名為 [escape] 的背景 <a>,用於關閉選單。

#sidenav-open {
  display: grid;
  grid-template-columns: [nav] 2fr [escape] 1fr;
}

調整 2fr1fr,找出選單疊加層和負空間關閉按鈕的合適比例。

示範變更比例時會發生什麼情況。

CSS 3D 轉換和轉場效果

現在,我們的版面配置會以行動裝置可視區域大小堆疊。在我新增一些新樣式之前, 預設會疊加在文章上。以下是我在下一節中要達成的 UX:

  • 動畫開啟和關閉
  • 只有在使用者同意的情況下,才使用動態效果
  • visibility 加入動畫效果,避免鍵盤焦點進入螢幕外元素

開始導入動態動畫時,我會優先考量無障礙功能。

無障礙動態效果

並非所有人都想要滑出動作體驗。在我們的解決方案中,這項偏好設定是透過調整媒體查詢內的 --duration CSS 變數套用。這個媒體查詢值代表使用者作業系統的動態效果偏好設定 (如有)。

#sidenav-open {
  --duration: .6s;
}

@media (prefers-reduced-motion: reduce) {
  #sidenav-open {
    --duration: 1ms;
  }
}
示範套用時間長度前後的互動。

現在,當側邊導覽列開啟和關閉時,如果使用者偏好減少動作,我會立即將元素移至檢視畫面,維持狀態但不移動。

轉場、變形、翻譯

側邊導覽列 (預設)

如要將行動裝置上側邊導覽列的預設狀態設為螢幕外狀態,請使用 transform: translateX(-110vw) 放置元素。

請注意,我在典型的螢幕外程式碼中新增了另一個 10vw,確保側邊導覽列的 box-shadow 隱藏時不會出現在主要檢視區塊中。-100vw

@media (max-width: 540px) {
  #sidenav-open {
    visibility: hidden;
    transform: translateX(-110vw);
    will-change: transform;
    transition:
      transform var(--duration) var(--easeOutExpo),
      visibility 0s linear var(--duration);
  }
}
側邊導覽列

#sidenav 元素符合 :target 時,請將 translateX() 位置設為 homebase 0, 並觀察 CSS 如何在網址雜湊變更時,將元素從 -110vw 的外部位置滑動至 var(--duration) 的「內部」位置 0

@media (max-width: 540px) {
  #sidenav-open:target {
    visibility: visible;
    transform: translateX(0);
    transition:
      transform var(--duration) var(--easeOutExpo);
  }
}

轉場瀏覽權限

現在的目標是隱藏螢幕閱讀器中的選單 (如果選單不在畫面上),這樣系統就不會將焦點放在畫面外的選單。我會在 :target 變更時設定顯示狀態轉換,藉此達成這個目標。

  • 進入時,請勿轉換顯示狀態,立即顯示,這樣我才能看到元素滑入並接受焦點。
  • 在離開時,轉換可見度但延遲,因此會在轉換結束時翻轉為 hidden

無障礙功能使用者體驗強化

這個解決方案會變更網址,以便管理狀態。 自然而然,這裡應該使用 <a> 元素,而且還能免費獲得一些實用的無障礙功能。讓我們為互動式元素加上清楚表達意圖的標籤。

<a href="#" id="sidenav-close" title="Close Menu" aria-label="Close Menu"></a>

<a href="#sidenav-open" id="sidenav-button" class="hamburger" title="Open Menu" aria-label="Open Menu">
  <svg>...</svg>
</a>
語音導覽和鍵盤互動使用者體驗的示範。

現在,滑鼠和鍵盤的主要互動按鈕都會清楚說明其用途。

:is(:hover, :focus)

這個實用的 CSS 函式虛擬選取器可讓我們快速納入懸停樣式,並與焦點共用這些樣式。

.hamburger:is(:hover, :focus) svg > line {
  stroke: hsl(var(--brandHSL));
}

加入 JavaScript

按下 escape 鍵即可關閉

鍵盤上的 Escape 鍵應該會關閉選單,對吧?讓我們接上電源。

const sidenav = document.querySelector('#sidenav-open');

sidenav.addEventListener('keyup', event => {
  if (event.code === 'Escape') document.location.hash = '';
});
瀏覽器歷史記錄

為避免開啟和關閉互動將多個項目堆疊到瀏覽器記錄中,請將下列 JavaScript 內嵌到關閉按鈕:

<a href="#" id="sidenav-close" title="Close Menu" aria-label="Close Menu" onchange="history.go(-1)"></a>

這樣一來,系統就會在關閉時移除網址記錄項目,就像選單從未開啟一樣。

Focus UX

下一個程式碼片段可協助我們在開啟或關閉按鈕後,將焦點放在這些按鈕上。我想輕鬆切換。

sidenav.addEventListener('transitionend', e => {
  const isOpen = document.location.hash === '#sidenav-open';

  isOpen
      ? document.querySelector('#sidenav-close').focus()
      : document.querySelector('#sidenav-button').focus();
})

開啟側邊導覽列後,將焦點移至關閉按鈕。側邊導覽列關閉時,請將焦點移至開啟按鈕。我在 JavaScript 中對元素呼叫 focus(),藉此完成這項操作。

結論

現在您已瞭解我的做法,您會怎麼做呢?這會產生一些有趣的元件架構!誰要製作第一個有插槽的版本?🙂

讓我們多元化地運用各種方法,瞭解在網路上建構內容的所有方式。建立 Glitch在 Twitter 上傳送你的版本給我,我會將其新增至下方的「社群混音」部分。

社群重混作品