Xây dựng thành phần breadcrumb (tập hợp liên kết phân cấp)

Thông tin tổng quan cơ bản về cách tạo một thành phần đường liên kết thích ứng và hỗ trợ tiếp cận để người dùng điều hướng trang web của bạn.

Trong bài đăng này, tôi muốn chia sẻ suy nghĩ về cách tạo thành phần đường liên kết. Dùng thử bản minh hoạ.

Bản minh hoạ

Nếu bạn thích xem video, thì đây là phiên bản video của bài đăng này trên YouTube:

Tổng quan

Thành phần đường dẫn breadcrumb cho biết vị trí của người dùng trong hệ thống phân cấp trang web. Tên này bắt nguồn từ câu chuyện Hansel và Gretel, trong đó hai nhân vật này đã thả các mẩu bánh mì phía sau trong một khu rừng tối tăm và có thể tìm đường về nhà bằng cách lần theo các mẩu bánh mì.

Đường liên kết dạng breadcrumb trong bài đăng này không phải là đường liên kết dạng breadcrumb tiêu chuẩn, mà là đường liên kết dạng breadcrumb. Các thành phần này cung cấp thêm chức năng bằng cách đặt các trang ngang hàng ngay vào chế độ điều hướng bằng <select>, giúp bạn có thể truy cập nhiều cấp.

Trải nghiệm người dùng ở chế độ nền

Trong video minh hoạ về thành phần ở trên, các danh mục phần giữ chỗ là thể loại trò chơi điện tử. Đường dẫn này được tạo bằng cách di chuyển theo đường dẫn sau: home » rpg » indie » on sale, như minh hoạ bên dưới.

Thành phần đường liên kết này phải cho phép người dùng di chuyển qua hệ thống phân cấp thông tin này; chuyển nhánh và chọn trang một cách nhanh chóng và chính xác.

Kiến trúc thông tin

Tôi thấy việc suy nghĩ theo hướng bộ sưu tập và mục là rất hữu ích.

Bộ sưu tập

Bộ sưu tập là một mảng các lựa chọn để bạn chọn. Trên trang chủ của nguyên mẫu đường dẫn liên kết của bài đăng này, các bộ sưu tập là FPS, RPG, brawler, dungeon crawler, thể thao và giải đố.

Mục

Trò chơi điện tử là một mặt hàng, một bộ sưu tập cụ thể cũng có thể là một mặt hàng nếu bộ sưu tập đó đại diện cho một bộ sưu tập khác. Ví dụ: RPG là một mục và là một bộ sưu tập hợp lệ. Khi đó là một mặt hàng, người dùng sẽ ở trên trang bộ sưu tập đó. Ví dụ: chúng nằm trên trang RPG, nơi hiển thị danh sách các trò chơi RPG, bao gồm cả các danh mục phụ bổ sung là AAA, Indie và Self Published.

Theo thuật ngữ khoa học máy tính, thành phần đường liên kết này biểu thị một mảng đa chiều:

const rawBreadcrumbData = {
  "FPS": {...},
  "RPG": {
    "AAA": {...},
    "indie": {
      "new": {...},
      "on sale": {...},
      "under 5": {...},
    },
    "self published": {...},
  },
  "brawler": {...},
  "dungeon crawler": {...},
  "sports": {...},
  "puzzle": {...},
}

Ứng dụng hoặc trang web của bạn sẽ có cấu trúc thông tin (IA) tuỳ chỉnh, tạo ra một mảng đa chiều khác, nhưng tôi hy vọng khái niệm về trang đích của bộ sưu tập và việc duyệt qua hệ phân cấp cũng có thể xuất hiện trong đường dẫn của bạn.

Bố cục

Markup (note: đây là tên ứng dụng)

Các thành phần hiệu quả bắt đầu bằng HTML phù hợp. Trong phần tiếp theo này, tôi sẽ trình bày các lựa chọn đánh dấu của mình và cách các lựa chọn đó ảnh hưởng đến thành phần tổng thể.

Bảng phối màu tối và sáng

<meta name="color-scheme" content="dark light">

Thẻ meta color-scheme trong đoạn mã trên thông báo cho trình duyệt biết rằng trang này muốn sử dụng các kiểu trình duyệt sáng và tối. Các đường liên kết trong ví dụ không có CSS cho các bảng phối màu này, vì vậy, các đường liên kết sẽ sử dụng màu mặc định do trình duyệt cung cấp.

<nav class="breadcrumbs" role="navigation"></nav>

Bạn nên sử dụng phần tử <nav> cho hoạt động điều hướng trên trang web. Phần tử này có vai trò ngầm định của ARIA là điều hướng. Trong quá trình kiểm thử, tôi nhận thấy rằng việc thay đổi thuộc tính role đã thay đổi cách trình đọc màn hình tương tác với phần tử, phần tử này thực sự được thông báo là điều hướng, vì vậy tôi đã chọn thêm thuộc tính này.

Biểu tượng

Khi một biểu tượng xuất hiện nhiều lần trên một trang, phần tử SVG <use> có nghĩa là bạn có thể xác định path một lần và sử dụng cho tất cả các phiên bản của biểu tượng. Điều này giúp ngăn thông tin về cùng một đường dẫn bị lặp lại, dẫn đến các tài liệu lớn hơn và khả năng đường dẫn không nhất quán.

Để sử dụng kỹ thuật này, hãy thêm một phần tử SVG ẩn vào trang và bao bọc các biểu tượng trong một phần tử <symbol> có mã nhận dạng duy nhất:

<svg style="display: none;">

  <symbol id="icon-home">
    <title>A home icon</title>
    <path d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/>
  </symbol>

  <symbol id="icon-dropdown-arrow">
    <title>A down arrow</title>
    <path d="M19 9l-7 7-7-7"/>
  </symbol>

</svg>

Trình duyệt đọc HTML SVG, đưa thông tin biểu tượng vào bộ nhớ và tiếp tục với phần còn lại của trang tham chiếu mã nhận dạng cho các mục đích sử dụng khác của biểu tượng, chẳng hạn như:

<svg viewBox="0 0 24 24" width="24" height="24" aria-hidden="true">
  <use href="#icon-home" />
</svg>

<svg viewBox="0 0 24 24" width="24" height="24" aria-hidden="true">
  <use href="#icon-dropdown-arrow" />
</svg>

Công cụ cho nhà phát triển cho thấy một phần tử sử dụng SVG đã kết xuất.

Bạn chỉ cần xác định một lần và có thể sử dụng bao nhiêu lần tuỳ thích mà không ảnh hưởng nhiều đến hiệu suất trang và có thể linh hoạt tạo kiểu. Lưu ý rằng aria-hidden="true" được thêm vào phần tử SVG. Các biểu tượng này không hữu ích cho những người chỉ nghe nội dung khi duyệt xem. Việc ẩn các biểu tượng này sẽ giúp những người dùng đó không bị làm phiền bởi những yếu tố không cần thiết.

Đây là điểm khác biệt giữa đường liên kết dạng đánh dấu truyền thống và đường liên kết trong thành phần này. Thông thường, đây sẽ chỉ là một đường liên kết <a>, nhưng tôi đã thêm UX truyền tải bằng một lựa chọn được che giấu. Lớp .crumb chịu trách nhiệm bố trí đường liên kết và biểu tượng, trong khi .crumbicon chịu trách nhiệm xếp biểu tượng và chọn phần tử lại với nhau. Tôi gọi đây là một đường liên kết phân tách vì các chức năng của đường liên kết này rất giống với một nút phân tách, nhưng dành cho hoạt động điều hướng trang.

<span class="crumb">
  <a href="#sub-collection-b">Category B</a>
  <span class="crumbicon">
    <svg>...</svg>
    <select class="disguised-select" title="Navigate to another category">
      <option>Category A</option>
      <option selected>Category B</option>
      <option>Category C</option>
    </select>
  </span>
</span>

Một đường liên kết và một số lựa chọn không có gì đặc biệt nhưng sẽ bổ sung thêm chức năng cho một đường dẫn đơn giản. Việc thêm title vào phần tử <select> sẽ giúp ích cho người dùng trình đọc màn hình, cung cấp cho họ thông tin về hành động của nút. Tuy nhiên, tính năng này cũng mang lại lợi ích cho mọi người khác, bạn sẽ thấy tính năng này xuất hiện ở vị trí nổi bật trên iPad. Một thuộc tính cung cấp ngữ cảnh nút cho nhiều người dùng.

Ảnh chụp màn hình cho thấy phần tử chọn không hiển thị đang được di chuột và chú thích theo bối cảnh của phần tử này đang hiển thị.

Đồ trang trí đường phân cách

<span class="crumb-separator" aria-hidden="true">→</span>

Bạn không bắt buộc phải thêm dấu phân cách, chỉ cần thêm một dấu phân cách cũng được (xem ví dụ thứ ba trong video ở trên). Sau đó, tôi sẽ đặt mỗi aria-hidden="true" vì chúng mang tính trang trí và không phải là nội dung mà trình đọc màn hình cần thông báo.

Thuộc tính gap (sẽ được đề cập tiếp theo) giúp việc điều chỉnh khoảng cách trở nên đơn giản.

Kiểu

Vì màu này sử dụng màu hệ thống, nên chủ yếu là các khoảng trống và ngăn xếp cho kiểu!

Hướng và luồng bố cục

Công cụ cho nhà phát triển cho thấy chế độ căn chỉnh điều hướng theo đường dẫn với tính năng lớp phủ flexbox.

Phần tử điều hướng chính nav.breadcrumbs đặt một thuộc tính tuỳ chỉnh có phạm vi để các phần tử con sử dụng, nếu không, phần tử này sẽ thiết lập một bố cục ngang được căn chỉnh theo chiều dọc. Điều này giúp đảm bảo các đường đánh dấu, đường phân chia và biểu tượng được căn chỉnh.

.breadcrumbs {
  --nav-gap: 2ch;

  display: flex;
  align-items: center;
  gap: var(--nav-gap);
  padding: calc(var(--nav-gap) / 2);
}

Một đường dẫn xuất hiện được căn chỉnh theo chiều dọc với các lớp phủ flexbox.

Mỗi .crumb cũng thiết lập một bố cục căn chỉnh theo chiều dọc và chiều ngang với một số khoảng trống, nhưng đặc biệt nhắm đến các phần tử con liên kết và chỉ định kiểu white-space: nowrap. Điều này rất quan trọng đối với đường dẫn breadcrumb có nhiều từ vì chúng ta không muốn đường dẫn này có nhiều dòng. Ở phần sau của bài đăng này, chúng ta sẽ thêm các kiểu để xử lý tình trạng tràn ngang do thuộc tính white-space này gây ra.

.crumb {
  display: inline-flex;
  align-items: center;
  gap: calc(var(--nav-gap) / 4);

  & > a {
    white-space: nowrap;

    &[aria-current="page"] {
      font-weight: bold;
    }
  }
}

aria-current="page" được thêm vào để giúp đường liên kết đến trang hiện tại nổi bật hơn so với các đường liên kết khác. Không chỉ giúp người dùng trình đọc màn hình có một chỉ báo rõ ràng rằng đường liên kết dành cho trang hiện tại, chúng tôi còn tạo kiểu trực quan cho phần tử này để giúp người dùng nhìn thấy có được trải nghiệm người dùng tương tự.

Thành phần .crumbicon dùng lưới để xếp chồng một biểu tượng SVG với một phần tử <select> "gần như vô hình".

Công cụ cho nhà phát triển Lưới xuất hiện khi phủ lên một nút có hàng và cột đều được đặt tên là ngăn xếp.

.crumbicon {
  --crumbicon-size: 3ch;

  display: grid;
  grid: [stack] var(--crumbicon-size) / [stack] var(--crumbicon-size);
  place-items: center;

  & > * {
    grid-area: stack;
  }
}

Phần tử <select> nằm ở cuối DOM, nên nằm ở đầu ngăn xếp và có thể tương tác. Thêm kiểu opacity: .01 để phần tử vẫn có thể sử dụng được và kết quả là một hộp chọn hoàn toàn phù hợp với hình dạng của biểu tượng. Đây là một cách hay để tuỳ chỉnh giao diện của một phần tử <select> trong khi vẫn duy trì chức năng tích hợp.

.disguised-select {
  inline-size: 100%;
  block-size: 100%;
  opacity: .01;
  font-size: min(100%, 16px); /* Defaults to 16px; fixes iOS zoom */
}

Trình đơn mục bổ sung

Đường dẫn nên có thể biểu thị một đường đi rất dài. Tôi thích cho phép các thành phần hiển thị theo chiều ngang khi thích hợp và tôi cảm thấy thành phần đường liên kết này đủ điều kiện.

.breadcrumbs {
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x proximity;
  scroll-padding-inline: calc(var(--nav-gap) / 2);

  & > .crumb:last-of-type {
    scroll-snap-align: end;
  }

  @supports (-webkit-hyphens:none) { & {
    scroll-snap-type: none;
  }}
}

Các kiểu tràn thiết lập UX sau:

  • Thao tác cuộn ngang có tính năng ngăn cuộn quá mức.
  • Khoảng đệm cuộn ngang.
  • Một điểm bám trên mảnh đường dẫn cuối cùng. Điều này có nghĩa là khi tải trang, đường dẫn đầu tiên sẽ tải nhanh và xuất hiện trong chế độ xem.
  • Xoá điểm chụp khỏi Safari, vì Safari gặp khó khăn với các tổ hợp hiệu ứng chụp và cuộn ngang.

Truy vấn về nội dung nghe nhìn

Một điểm điều chỉnh nhỏ cho các khung hiển thị nhỏ hơn là ẩn nhãn "Trang chủ", chỉ để lại biểu tượng:

@media (width <= 480px) {
  .breadcrumbs .home-label {
    display: none;
  }
}

So sánh đường dẫn với và không có nhãn trang chủ.

Hỗ trợ tiếp cận

Chuyển động

Thành phần này không có nhiều chuyển động, nhưng bằng cách bao bọc hiệu ứng chuyển đổi trong một lệnh kiểm tra prefers-reduced-motion, chúng ta có thể ngăn chặn chuyển động không mong muốn.

@media (prefers-reduced-motion: no-preference) {
  .crumbicon {
    transition: box-shadow .2s ease;
  }
}

Không cần thay đổi bất kỳ kiểu nào khác, hiệu ứng di chuột và lấy tiêu điểm rất phù hợp và có ý nghĩa mà không cần transition, nhưng nếu chuyển động được phép, chúng ta sẽ thêm một hiệu ứng chuyển đổi tinh tế vào hoạt động tương tác.

JavaScript

Trước tiên, bất kể loại bộ định tuyến mà bạn sử dụng trong trang web hoặc ứng dụng của mình, khi người dùng thay đổi đường dẫn, URL cần được cập nhật và người dùng sẽ thấy trang thích hợp. Thứ hai, để chuẩn hoá trải nghiệm người dùng, hãy đảm bảo không có thao tác điều hướng bất ngờ nào xảy ra khi người dùng chỉ duyệt xem các lựa chọn <select>.

Hai chỉ số quan trọng về trải nghiệm người dùng mà JavaScript cần xử lý: select has changed và eager <select> change event firing prevention.

Bạn cần ngăn chặn sự kiện quá sớm do sử dụng một phần tử <select>. Trên Windows Edge và có thể là cả các trình duyệt khác, sự kiện chọn changed sẽ kích hoạt khi người dùng duyệt qua các lựa chọn bằng bàn phím. Đó là lý do tôi gọi nó là "háo hức", vì người dùng chỉ chọn lựa chọn giả, chẳng hạn như di chuột hoặc lấy tiêu điểm, nhưng chưa xác nhận lựa chọn bằng enter hoặc click. Sự kiện eager khiến người dùng không thể truy cập vào tính năng thay đổi danh mục thành phần này, vì việc mở hộp chọn và chỉ cần duyệt xem một mục sẽ kích hoạt sự kiện và thay đổi trang trước khi người dùng sẵn sàng.

Sự kiện <select> đã thay đổi theo hướng tích cực hơn

const crumbs = document.querySelectorAll('.breadcrumbs select')
const allowedKeys = new Set(['Tab', 'Enter', ' '])
const preventedKeys = new Set(['ArrowUp', 'ArrowDown'])

// watch crumbs for changes,
// ensures it's a full value change, not a user exploring options via keyboard
crumbs.forEach(nav => {
  let ignoreChange = false

  nav.addEventListener('change', e => {
    if (ignoreChange) return
    // it's actually changed!
  })

  nav.addEventListener('keydown', ({ key }) => {
    if (preventedKeys.has(key))
      ignoreChange = true
    else if (allowedKeys.has(key))
      ignoreChange = false
  })
})

Chiến lược cho việc này là theo dõi các sự kiện nhấn phím trên mỗi phần tử <select> và xác định xem phím được nhấn có phải là phím xác nhận thao tác điều hướng (Tab hoặc Enter) hay phím điều hướng không gian (ArrowUp hoặc ArrowDown) hay không. Khi xác định được, thành phần có thể quyết định chờ hoặc chuyển sang trạng thái tiếp theo khi sự kiện cho phần tử <select> kích hoạt.

Kết luận

Giờ bạn đã biết cách tôi làm, vậy bạn sẽ làm như thế nào? 🙂

Hãy đa dạng hoá các phương pháp và tìm hiểu tất cả các cách để xây dựng trên web. Hãy tạo một bản minh hoạ, gửi đường liên kết cho tôi qua Twitter và tôi sẽ thêm bản minh hoạ đó vào phần bản phối lại của cộng đồng bên dưới!

Bản phối lại của cộng đồng