建立分頁元件

基礎總覽:如何建構類似於 iOS 和 Android 應用程式中的分頁元件。

在這篇文章中,我想分享如何為網頁建構「分頁」元件,使其具備回應性、支援多種裝置輸入方式,且適用於各種瀏覽器。試用示範模式

示範

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

總覽

分頁是設計系統的常見元件,但可以採用多種形狀和形式。首先是根據 <frame> 元素建構的電腦版分頁,現在則有根據物理屬性為內容加上動畫效果的流暢行動版元件。但目的都是一樣:節省空間。

目前,分頁使用者體驗的要點是按鈕導覽區域,可切換顯示框架中內容的顯示狀態。許多不同的內容區域共用同一個空間,但會根據導覽中選取的按鈕有條件地顯示。

由於網頁對元件概念套用了各種樣式,拼貼圖片相當混亂
過去 10 年來,分頁元件網頁設計風格的拼貼

網站策略

總而言之,由於有幾項重要的網頁平台功能,我發現這個元件相當容易建構:

  • scroll-snap-points,可優雅地滑動及透過鍵盤互動,並適當停止捲動
  • 透過網址雜湊的深層連結,支援瀏覽器處理的頁內捲動錨點和分享功能
  • 使用 <a>id="#hash" 元素標記支援螢幕閱讀器
  • prefers-reduced-motion,啟用淡入淡出轉場效果和網頁內即時捲動功能
  • 草案中的 @scroll-timeline 網頁功能,可動態加上底線並變更所選分頁的顏色

HTML

從根本上來說,這裡的 UX 是:點選連結、讓網址代表巢狀網頁狀態,然後在瀏覽器捲動至相符元素時,看到內容區域更新。

其中包含一些結構化內容成員:連結和 :target。我們需要連結清單 (<nav> 很適合),以及元素清單 (<section> 很適合)。<article>每個連結雜湊都會對應一個區段,讓瀏覽器透過錨定捲動內容。

點選連結按鈕,滑入焦點內容

舉例來說,在 Chrome 89 中點按連結時,系統會自動聚焦 :target 文章,不需使用 JS。使用者可以照常使用輸入裝置捲動文章內容。如標記所示,這是免費內容。

我使用下列標記整理分頁:

<snap-tabs>
  <header>
    <nav>
      <a></a>
      <a></a>
      <a></a>
      <a></a>
    </nav>
  </header>
  <section>
    <article></article>
    <article></article>
    <article></article>
    <article></article>
  </section>
</snap-tabs>

我可以透過 hrefid 屬性,建立 <a><article> 元素之間的連結,如下所示:

<snap-tabs>
  <header>
    <nav>
      <a href="#responsive"></a>
      <a href="#accessible"></a>
      <a href="#overscroll"></a>
      <a href="#more"></a>
    </nav>
  </header>
  <section>
    <article id="responsive"></article>
    <article id="accessible"></article>
    <article id="overscroll"></article>
    <article id="more"></article>
  </section>
</snap-tabs>

接著,我為文章填入不同數量的亂數假文,並為連結設定不同長度和圖片的標題。有了可使用的內容,我們就能開始排版。

捲動版面配置

這個元件有 3 種不同的捲動區域:

  • 導覽列 (粉紅色) 可水平捲動
  • 內容區域 (藍色) 可水平捲動
  • 每個文章項目 (綠色) 都可以垂直捲動。
3 個彩色方塊,附有顏色相符的方向箭頭,標示捲動區域並顯示捲動方向。

捲動功能涉及 2 種不同類型的元素:

  1. 視窗
    具有已定義尺寸的方塊,具有 overflow 屬性樣式。
  2. 過大的介面
    在這個版面配置中,這是指清單容器:導覽連結、文章區段和文章內容。

<snap-tabs>」版面配置

我選擇的頂層版面配置是彈性 (Flexbox)。我將方向設為 column,因此標題和區段會垂直排序。這是我們的第一個捲動視窗,會隱藏所有溢位內容。標頭和區段很快就會採用過度捲動,做為個別區域。

HTML
<snap-tabs>
  <header></header>
  <section></section>
</snap-tabs>
CSS
  snap-tabs {
  display: flex;
  flex-direction: column;

  /* establish primary containing box */
  overflow: hidden;
  position: relative;

  & > section {
    /* be pushy about consuming all space */
    block-size: 100%;
  }

  & > header {
    /* defend against 
needing 100% */ flex-shrink: 0; /* fixes cross browser quarks */ min-block-size: fit-content; } }

回顧彩色 3 捲軸圖表:

  • <header> 現在已準備好做為(粉紅色)捲動容器。
  • <section> 已準備好成為藍色捲動容器。

我已在下方以 VisBug 標示的影格,可協助我們查看捲動容器建立的視窗

標頭和區段元素會疊加桃紅色,標示這些元素在元件中占用的空間

分頁 <header> 版面配置

下一個版面配置幾乎相同:我使用 Flex 建立垂直排序。

HTML
<snap-tabs>
  <header>
    <nav></nav>
    <span class="snap-indicator"></span>
  </header>
  <section></section>
</snap-tabs>
CSS
header {
  display: flex;
  flex-direction: column;
}

.snap-indicator 應與連結群組水平移動,而這個標題版面配置有助於設定該階段。這裡沒有絕對位置元素!

nav 和 span.indicator 元素上都有桃紅色疊加層,標示出這些元素在元件中占用的空間

接著是捲動樣式。結果發現我們可以在 2 個水平捲動區域 (標頭和區段) 之間共用捲動樣式,因此我建立了一個公用程式類別 .scroll-snap-x

.scroll-snap-x {
  /* browser decide if x is ok to scroll and show bars on, y hidden */
  overflow: auto hidden;
  /* prevent scroll chaining on x scroll */
  overscroll-behavior-x: contain;
  /* scrolling should snap children on x */
  scroll-snap-type: x mandatory;

  @media (hover: none) {
    scrollbar-width: none;

    &::-webkit-scrollbar {
      width: 0;
      height: 0;
    }
  }
}

每個項目都需要 X 軸溢位、捲動容器來捕捉過度捲動、觸控裝置的隱藏捲軸,以及鎖定內容呈現區域的捲動捕捉。我們的鍵盤分頁順序可供存取,任何互動都會自然引導焦點。捲動快照容器也會透過鍵盤取得輪轉介面樣式的互動。

分頁標題 <nav> 版面配置

導覽連結必須排成一行,不得斷行,垂直置中對齊,且每個連結項目都應貼齊捲動貼齊容器。Swift work for 2021 CSS!

HTML
<nav>
  <a></a>
  <a></a>
  <a></a>
  <a></a>
</nav>
CSS
  nav {
  display: flex;

  & a {
    scroll-snap-align: start;

    display: inline-flex;
    align-items: center;
    white-space: nowrap;
  }
}

每個連結都會自行設定樣式和大小,因此導覽版面配置只需要指定方向和流程。導覽項目的寬度各不相同,因此指標會調整寬度以配合新目標,讓分頁之間的轉場效果更有趣。視其中的元素數量而定,瀏覽器會顯示捲軸。

導覽列的 a 元素會疊加桃紅色,標示出元素在元件中占用的空間,以及溢位的位置

分頁 <section> 版面配置

這個區段是彈性項目,必須是空間的主要取用端。此外,還需要為要放置的文章建立欄位。再次感謝您為 2021 年 CSS 提供的協助!block-size: 100% 會盡可能延展這個元素來填滿父項,然後為自己的版面配置建立一系列100%父項寬度的欄。由於我們已對父項設定嚴格的限制,因此百分比在這裡非常適用。

HTML
<section>
  <article></article>
  <article></article>
  <article></article>
  <article></article>
</section>
CSS
  section {
  block-size: 100%;

  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 100%;
}

這就像是說「盡可能以強勢的方式垂直擴展」(請記得我們將標頭設為 flex-shrink: 0:這是為了防範這種擴展推動),這會為一組全高欄設定列高。auto-flow 樣式會告知格線一律以水平線排列子項,且不會換行,這正是我們想要的;溢出父項視窗。

文章元素會疊加桃紅色,標示元素在元件中占用的空間,以及溢位的位置

有時我會覺得這些概念很難理解!這個區段元素會裝入盒子,但也會建立一組盒子。希望這些圖片和說明對您有所幫助。

分頁 <article> 版面配置

使用者應可捲動文章內容,且只有在溢位時才會顯示捲軸。這些文章元素整齊排列。它們同時是捲動父項和捲動子項。瀏覽器會為我們處理一些棘手的觸控、滑鼠和鍵盤互動。

HTML
<article>
  <h2></h2>
  <p></p>
  <p></p>
  <h2></h2>
  <p></p>
  <p></p>
  ...
</article>
CSS
article {
  scroll-snap-align: start;

  overflow-y: auto;
  overscroll-behavior-y: contain;
}

我選擇讓文章在父項捲動器內對齊。我非常喜歡導覽連結項目和文章元素如何對齊各自捲動容器的行內開頭。這段關係看起來和諧,相處起來也很融洽。

文章元素及其子項元素會以桃紅色疊加顯示,標示出這些元素在元件中占用的空間,以及溢位方向

這篇文章是格線子項,大小預先決定為要提供捲動 UX 的檢視區塊區域。這表示我不需要任何高度或寬度樣式,只要定義溢位方式即可。我將 overflow-y 設為 auto, 然後使用方便的 overscroll-behavior 屬性,擷取捲動互動。

3 個捲動區域回顧

下方是我在系統設定中選擇「一律顯示捲軸」的畫面。我認為,如果版面配置要搭配這項設定運作,就必須加倍重要,因為我需要檢查版面配置和捲動協調。

3 個捲軸都設為顯示,現在會佔用版面配置空間,但元件看起來還是很棒

我認為在這個元件中顯示捲軸側欄,有助於清楚顯示捲動區域、支援的方向,以及彼此之間的互動方式。請注意,這些捲動視窗框架也是版面配置的彈性或格線父項。

開發人員工具可協助我們以視覺化方式呈現這項資訊:

捲動區域有格線和 Flexbox 工具疊加層,會標示出這些區域在元件中占用的空間,以及溢位方向
Chromium 開發人員工具,顯示包含錨點元素的彈性方塊導覽元素版面配置、包含文章元素的格線區段版面配置,以及包含段落和標題元素的文章元素。

捲動版面配置已完成:可快速移動、可深層連結,且可透過鍵盤存取。奠定穩固基礎,提升使用者體驗、風格和樂趣。

功能重點

捲動時,子項會維持鎖定位置,不會在調整大小時移動。這表示 JavaScript 不需要在裝置旋轉或瀏覽器調整大小時,將任何內容帶入檢視畫面。在 Chromium 開發人員工具的裝置模式中試用,選取「Responsive」(回應式) 以外的任何模式,然後調整裝置框架大小。請注意,元素會保持在檢視畫面中,並鎖定其內容。Chromium 更新實作項目以符合規格後,這項功能就已推出。如要瞭解詳情,請參閱這篇網誌文章

動畫

這裡的動畫工作目標是清楚連結互動與 UI 回饋。這有助於引導或協助使用者順暢地探索所有內容。我會根據目的和條件加入動態效果。使用者現在可以在作業系統中指定動態效果偏好設定,而我非常樂意在介面中回應他們的偏好設定。

我會將分頁底線與文章捲動位置連結。對齊不僅是美觀的對齊方式,也是動畫的開始和結束錨點。這樣一來,<nav> (可當做迷你地圖) 就會與內容保持連結。我們會從 CSS 和 JS 檢查使用者的動態偏好設定。有幾個地方需要注意!

捲動行為

:targetelement.scrollIntoView() 的動作行為都有提升空間。預設為即時。瀏覽器只會設定捲動位置。如果我們想改為轉移到該捲動位置,而不是閃爍,該怎麼做?

@media (prefers-reduced-motion: no-preference) {
  .scroll-snap-x {
    scroll-behavior: smooth;
  }
}

由於我們在此處導入動作,且使用者無法控制動作 (例如捲動),因此只有在使用者作業系統未偏好減少動作時,我們才會套用此樣式。這樣一來,我們只會向願意接受捲動效果的使用者顯示。

分頁指標

這項動畫的目的是協助將指標與內容狀態建立關聯。我決定為偏好減少動作的使用者採用色彩交叉淡出 border-bottom 樣式,並為可接受動作的使用者採用捲動連結的滑動 + 色彩淡出動畫。

在 Chromium 開發人員工具中,我可以切換偏好設定,並展示 2 種不同的轉場效果。我非常享受建構這個過程。

@media (prefers-reduced-motion: reduce) {
  snap-tabs > header a {
    border-block-end: var(--indicator-size) solid hsl(var(--accent) / 0%);
    transition: color .7s ease, border-color .5s ease;

    &:is(:target,:active,[active]) {
      color: var(--text-active-color);
      border-block-end-color: hsl(var(--accent));
    }
  }

  snap-tabs .snap-indicator {
    visibility: hidden;
  }
}

如果使用者偏好減少動作,我會隱藏 .snap-indicator,因為我不再需要這個圖示。然後以 border-block-end 樣式和 transition 取代。此外,請注意,在分頁互動中,除了品牌底線醒目顯示外,使用中的導覽項目文字顏色也較深。使用中的元素具有較高的文字色彩對比度,以及明亮的底光強調色。

只要多幾行 CSS,就能讓使用者感受到尊重 (我們貼心地尊重他們的動作偏好設定)。我喜歡。

@scroll-timeline

我在上一節中說明如何處理減少動作的交叉淡出樣式,本節則會說明如何將指標和捲動區域連結在一起。接下來要介紹一些有趣的實驗性功能。希望你跟我一樣興奮。

const { matches:motionOK } = window.matchMedia(
  '(prefers-reduced-motion: no-preference)'
);

我會先透過 JavaScript 檢查使用者的動作偏好設定。如果結果為 false,表示使用者偏好減少動作,我們就不會執行任何捲動連結動作效果。

if (motionOK) {
  // motion based animation code
}

撰寫本文時,瀏覽器不支援@scroll-timeline。這份草案規格僅包含實驗性實作項目。不過,它有 Polyfill,我會在這次的示範中使用。

ScrollTimeline

CSS 和 JavaScript 都能建立捲動時間軸,但我選擇使用 JavaScript,這樣就能在動畫中使用即時元素測量。

const sectionScrollTimeline = new ScrollTimeline({
  scrollSource: tabsection,  // snap-tabs > section
  orientation: 'inline',     // scroll in the direction letters flow
  fill: 'both',              // bi-directional linking
});

我希望 1 個項目跟隨另一個項目的捲動位置,因此我建立 ScrollTimeline 來定義捲動連結的驅動程式,也就是 scrollSource。一般來說,網頁上的動畫會根據全域時間碼執行,但如果記憶體中有自訂 sectionScrollTimeline,我就可以變更所有內容。

tabindicator.animate({
    transform: ...,
    width: ...,
  }, {
    duration: 1000,
    fill: 'both',
    timeline: sectionScrollTimeline,
  }
);

在深入瞭解動畫的關鍵影格之前,我想先指出,捲動的追蹤器 tabindicator 會根據自訂時間軸 (也就是我們區段的捲動) 產生動畫。這樣就完成連結,但還缺少最後一個要素:要動畫化的有狀態點,也就是所謂的關鍵影格。

動態主要畫面格

使用 @scroll-timeline 製作動畫時,可以採用功能強大的純宣告式 CSS 方式,但我選擇的動畫太過動態。無法在 auto 寬度之間轉換,也無法根據子項長度動態建立多個關鍵影格。

不過,JavaScript 知道如何取得這項資訊,因此我們會自行疊代子項,並在執行階段擷取計算值:

tabindicator.animate({
    transform: [...tabnavitems].map(({offsetLeft}) =>
      `translateX(${offsetLeft}px)`),
    width: [...tabnavitems].map(({offsetWidth}) =>
      `${offsetWidth}px`)
  }, {
    duration: 1000,
    fill: 'both',
    timeline: sectionScrollTimeline,
  }
);

針對每個 tabnavitem,解構 offsetLeft 位置並傳回字串,將其做為 translateX 值。這會為動畫建立 4 個轉換主要畫面格。寬度也是如此,系統會詢問每個寬度的動態寬度,然後將其做為關鍵影格值。

以下是根據我的字型和瀏覽器偏好設定輸出的範例:

TranslateX 主要畫面格:

[...tabnavitems].map(({offsetLeft}) =>
  `translateX(${offsetLeft}px)`)

// results in 4 array items, which represent 4 keyframe states
// ["translateX(0px)", "translateX(121px)", "translateX(238px)", "translateX(464px)"]

寬度關鍵影格:

[...tabnavitems].map(({offsetWidth}) =>
  `${offsetWidth}px`)

// results in 4 array items, which represent 4 keyframe states
// ["121px", "117px", "226px", "67px"]

簡單來說,分頁指標現在會根據區段捲動器的捲動貼齊位置,在 4 個關鍵影格之間產生動畫。吸附點可清楚劃分主要畫面格,並讓動畫呈現同步感。

使用中的分頁和閒置分頁會顯示 VisBug 疊加層,兩者都通過對比度分數

使用者可透過互動控制動畫,看到指標的寬度和位置從一個區段變更到下一個區段,並與捲動完美同步。

您可能沒注意到,但當醒目顯示的導覽項目遭到選取時,我對色彩的轉變感到非常自豪。

如果醒目顯示的項目對比度較高,未選取的淺灰色項目會更不明顯。文字的顏色通常會在懸停和選取時轉換,但如果能與底線指標同步,在捲動時轉換顏色,就是更進階的做法。

我的做法如下:

tabnavitems.forEach(navitem => {
  navitem.animate({
      color: [...tabnavitems].map(item =>
        item === navitem
          ? `var(--text-active-color)`
          : `var(--text-color)`)
    }, {
      duration: 1000,
      fill: 'both',
      timeline: sectionScrollTimeline,
    }
  );
});

每個分頁導覽連結都需要這個新顏色動畫,並追蹤與底線指標相同的捲動時間軸。我使用與先前相同的時間軸:由於時間軸的角色是在捲動時發出刻度,因此我們可以在任何類型的動畫中使用該刻度。如同先前,我在迴圈中建立 4 個影格,並傳回顏色。

[...tabnavitems].map(item =>
  item === navitem
    ? `var(--text-active-color)`
    : `var(--text-color)`)

// results in 4 array items, which represent 4 keyframe states
// [
  "var(--text-active-color)",
  "var(--text-color)",
  "var(--text-color)",
  "var(--text-color)",
]

顏色為 var(--text-active-color) 的關鍵影格會醒目顯示連結,否則就是標準文字顏色。由於有巢狀迴圈,這項作業相對簡單,因為外部迴圈是每個導覽項目,內部迴圈則是每個導覽項目的個人化關鍵影格。我會檢查外層迴圈元素是否與內層迴圈元素相同,並據此判斷何時選取元素。

我寫這首歌時非常開心,愛死了

更多 JavaScript 強化功能

請注意,我在此展示的內容核心不需要 JavaScript 就能運作。話不多說,讓我們看看如何在 JavaScript 可用時強化這項功能。

深層連結是行動裝置專用的詞彙,但我認為這裡的索引標籤可滿足深層連結的意圖,因為您可以直接分享索引標籤內容的網址。瀏覽器會透過網址雜湊中相符的 ID,在頁面內導覽。我發現這個 onload 處理常式可在各平台產生效果。

window.onload = () => {
  if (location.hash) {
    tabsection.scrollLeft = document
      .querySelector(location.hash)
      .offsetLeft;
  }
}

捲動結束同步

使用者不一定會點選或使用鍵盤,有時只是自由捲動,這也是他們應有的權利。當區段捲動器停止捲動時,無論停在何處,都必須與頂端導覽列中的項目相符。

以下是我等待捲動結束的方式: js tabsection.addEventListener('scroll', () => { clearTimeout(tabsection.scrollEndTimer); tabsection.scrollEndTimer = setTimeout(determineActiveTabSection, 100); });

每當捲動區段時,請清除區段逾時 (如有),然後啟動新的逾時。當區段停止捲動時,請勿清除逾時,並在靜止 100 毫秒後觸發。觸發時,請呼叫函式,找出使用者停止的位置。

const determineActiveTabSection = () => {
  const i = tabsection.scrollLeft / tabsection.clientWidth;
  const matchingNavItem = tabnavitems[i];

  matchingNavItem && setActiveTab(matchingNavItem);
};

假設捲動已對齊,將目前的捲動位置除以捲動區域的寬度,應會得到整數,而非小數。然後,我嘗試透過這個計算出的索引,從快取擷取 navitem,如果找到任何項目,我會傳送相符項目以設為有效。

const setActiveTab = tabbtn => {
  tabnav
    .querySelector(':scope a[active]')
    .removeAttribute('active');

  tabbtn.setAttribute('active', '');
  tabbtn.scrollIntoView();
};

設定使用中分頁時,系統會先清除目前的使用中分頁,然後為傳入的導覽項目提供使用中狀態屬性。呼叫 scrollIntoView() 與 CSS 的互動很有趣,值得注意。

.scroll-snap-x {
  overflow: auto hidden;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;

  @media (prefers-reduced-motion: no-preference) {
    scroll-behavior: smooth;
  }
}

在水平捲動快照公用程式 CSS 中,我們巢狀處理了媒體查詢,如果使用者可接受動作,就會套用smooth捲動。JavaScript 可以自由呼叫捲動元素進入檢視畫面,而 CSS 可以宣告式地管理 UX。有時他們會成為一對可愛的小搭檔。

結論

現在您已瞭解我的做法,您會怎麼做呢?這讓元件架構變得很有趣!誰要用自己最愛的架構,製作第一個含有插槽的版本?🙂

讓我們多元化方法,學習在網路上建構內容的所有方式。 建立 Glitch,然後在 Twitter 上提及我, 並附上你的版本,我會將其新增至下方的「社群混音」 部分。

社群重混作品