建立分頁元件

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

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

示範

如果你偏好觀看影片,請參閱這篇文章的 YouTube 版本:

總覽

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

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

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

網路策略

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

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

HTML

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

其中包含一些結構性內容成員:連結和 :target。我們需要連結清單,<nav> 非常適合用於這類清單,而 <article> 元素清單則適合用於 <section>。每個連結雜湊都會與某個區段相符,讓瀏覽器透過錨點捲動內容。

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

舉例來說,點選連結時,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 支援 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> 版面配置

這個區段是 Flex 項目,需要是空間的主要使用者。也需要建立資料欄,用於放置文章。再次強調,Swift 適用於 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;
}

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

article 元素及其子項元素會套用粉紅色疊加層,標示這些元素在元件中占用的空間,以及溢出的方向

文章是格線子項,其大小已預先設定為我們要提供捲動使用者體驗的檢視區域。這表示我不需要任何高度或寬度樣式,只需要定義溢位方式。我將 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,將根據自訂時間軸 (即本節的捲動) 播放動畫。這會完成連結,但缺少最終元素:狀態點,也就是用於動畫轉換的狀態點,也稱為關鍵影格。

動態主要畫面格

有一種非常強大的純宣告式 CSS 方式,可透過 @scroll-timeline 製作動畫,但我選擇的動畫太生動了。無法在 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,並在推特上傳你的版本,我會將其加入下方的「社群混音」部分。

社群重混作品