建立分頁元件

概略說明如何建立類似 iOS 和 Android 應用程式中的分頁元件。

在這篇文章中,我想分享為網路建構分頁元件的想法,這個元件可回應式、支援多種裝置輸入,並可跨瀏覽器運作。試用示範模式

示範

如果您喜歡看影片,請參考這篇文章的 YouTube 版本:

總覽

分頁是設計系統的常見元件,但可以採用多種形狀和格式。首先,我們在 <frame> 元素上建立了電腦版分頁,現在則推出了流暢的行動版元件,可根據物理屬性為內容製作動畫。它們都試圖做同一件事情:節省空間。

如今,分頁使用者體驗的基本概念是按鈕導覽區域,用於切換顯示影格中內容的顯示設定。許多不同的內容區域共用相同的空間,但會根據導覽選單中選取的按鈕條件式顯示。

由於網頁已將多種樣式套用至元件概念,因此拼貼畫面相當混亂
過去 10 年來,分頁元件網頁設計樣式的美術拼貼

網路策略

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

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

HTML

基本上,這裡的使用者體驗為:按一下連結,使用網址表示巢狀頁面狀態,然後在瀏覽器捲動至相符元素時查看內容區域更新。

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

使用者點選連結按鈕,可在聚焦內容中滑動

舉例來說,點選連結會自動將 :target 文章焦點移至 Chrome 89,無須使用 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 支援 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> 版面配置

本節是彈性商品,需要是空間的主要消費者。還必須建立資料欄,才能放置文章。順帶一提,這項快速做法 適用於 CSS 2021!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;
}

我選擇讓文章在其父項捲軸中對齊。我很喜歡導覽連結項目和文章元素如何對齊各自捲動容器的內嵌起始位置。看起來和感覺上都像是和諧的關係。

文章元素及其子項元素重疊在一起,以概述這些在元件中所佔的空間,以及這些元素溢出的方向

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

3 個捲動區域重點總覽

在下方,我已在系統設定中選擇「一律顯示捲軸」。我認為,在開啟這項設定的情況下,讓版面配置正常運作非常重要,因為我需要查看版面配置和捲動協調作業。

3 個捲軸都已設定為顯示,現在會占用版面配置空間,但元件仍會正常顯示

我認為在這個元件中顯示捲軸邊框有助於清楚顯示捲動區域的位置、支援的方向,以及彼此的互動方式。請考量每個捲動視窗框架如何同時是版面配置的 Flex 或 GridLayout 父項。

開發人員工具可協助我們以圖像呈現這項資訊:

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

捲動版面配置已完成:對齊、可建立深層連結,以及可透過鍵盤存取。奠定使用者體驗強化、風格和愉悅感的穩固基礎。

功能醒目顯示

捲動時,已固定的子項會在調整大小時維持鎖定位置。這表示 JavaScript 在裝置旋轉或瀏覽器調整大小時,不需要將任何內容顯示在畫面上。在 Chromium 開發人員工具的「Device Mode」中試用這項功能,方法是選取「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 即可運作。話雖如此說,讓我們來看看在 JS 可用時,我們如何加以改善。

深層連結通常比行動術語,但我認為深層連結的用意就在於可透過分頁,直接分享分頁內容的網址。瀏覽器會在網頁中前往網址雜湊中相符的 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 則可宣告式地管理使用者體驗。有時他們會進行精彩的對決。

結論

既然你知道我如何做到,你會怎麼做呢?這會產生一些有趣的元件架構!誰會在自己偏好的架構中,製作第一個含有空格的版本?🙂

讓我們來體驗更多元的方法,並瞭解在網路上建立內容的所有方式。 建立 Glitch,並在 Twitter 上傳送你的版本,我會將其加入下方的「社群混音」部分。

社群重混作品