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 thành phần đường dẫn dạng chuỗi liên kết thích ứng và hỗ trợ tiếp cận để người dùng điều hướng trên trang web của bạn.

Trong bài đăng này, tôi muốn chia sẻ cách tạo thành phần breadcrumb (tập hợp liên kết phân cấp). Dùng thử bản minh hoạ.

Bản minh hoạ

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

Tổng quan

Thành phần breadcrumbs (dấu vết) cho biết vị trí của người dùng trong hệ 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 đã rắc bánh mì vụn phía sau họ trong một số khu rừng tối và có thể tìm đường về nhà bằng cách lần theo dấu vết bánh mì.

Các đường dẫn liên kết phân cấp trong bài đăng này không phải là đường dẫn liên kết phân cấp tiêu chuẩn mà là đường dẫn liên kết phân cấp kiểu. Các thành phần này cung cấp chức năng bổ sung bằng cách đưa các trang đồng cấp vào thành phần điều hướng bằng <select>, cho phép truy cập nhiều cấp.

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

Trong video minh hoạ 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ử. Dấu vết này được tạo bằng cách điều hướng theo đường dẫn sau: home » rpg » indie » on sale, như minh hoạ bên dưới.

Thành phần đường dẫn này 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 sang các nhánh và chọn các 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 bộ sưu tập và mục rất hữu ích.

Bộ sưu tập

Bộ sưu tập là một mảng gồm các tuỳ chọn để lựa chọn. Trên trang chủ của nguyên mẫu đường dẫn dạng chuỗi trong bài đăng này, các bộ sưu tập là FPS, RPG, brawler, dungeon crawler, thể thao và câu đố.

Mục

Trò chơi điện tử là một mục, một bộ sưu tập cụ thể cũng có thể là một mục 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à một bộ sưu tập hợp lệ. Khi đó là một mặt hàng, người dùng đang ở trang bộ sưu tập đó. Ví dụ: các danh mục phụ này nằm trên trang Trò chơi nhập vai, trong đó có danh sách các trò chơi nhập vai, bao gồm cả các danh mục phụ bổ sung AAA, Indie và Tự xuất bản.

Theo khoa học máy tính, thành phần breadcrumb (tập hợp liên kết phân cấp) này đại diện cho 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 tuỳ chỉnh (IA) tạo ra một mảng nhiều 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à duyệt qua hệ phân cấp cũng có thể đưa vào đườ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 tốt bắt đầu bằng HTML phù hợp. Trong phần tiếp theo, tôi sẽ trình bày các lựa chọn về mã đánh dấu và mức độ tác động của các lựa chọn đó đế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 rằng trang này muốn có kiểu trình duyệt sáng và tối. Dấu vết bánh mì mẫu không bao gồm bất kỳ CSS nào cho các bảng phối màu này, vì vậy, dấu vết bánh mì 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> để điều hướng trang web. Phần tử này có vai trò điều hướng ARIA ngầm ẩn. Trong quá trình kiểm thử, tôi nhận thấy rằng việc có 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ử, thuộc tính 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 được lặp lại 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 nó cho tất cả các thực thể của biểu tượng. Điều này giúp tránh lặp lại cùng một thông tin đường dẫn, gây ra tài liệu lớn hơn và có thể dẫn đến tình trạ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 bị ẩn vào trang và gói các biểu tượng đó trong một phần tử <symbol> bằng một 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 để sử dụng thêm biểu tượng, như sau:

<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 phần tử sử dụng SVG được kết xuất.

Xác định một lần, sử dụng nhiều lần như bạn muốn, với mức tác động tối thiểu đến hiệu suất trang và kiểu linh hoạt. Lưu ý 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 duyệt web chỉ nghe thấy nội dung, việc ẩn chúng khỏi những người dùng đó sẽ ngăn họ gây thêm tiếng ồn không cần thiết.

Đây là điểm khác biệt giữa đường dẫn truyền thống và đường dẫn 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 trải nghiệm người dùng xuyên suốt bằng một lựa chọn được ngụy trang. 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 chồng biểu tượng và phần tử chọn cùng nhau. Tôi gọi đó là đườ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 nút phân tách, nhưng dành cho việc đ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ố tuỳ chọn không có gì đặc biệt nhưng lại thêm chức năng vào một đường dẫn đơn giản. Việc thêm title vào phần tử <select> sẽ hữu ích cho người dùng trình đọc màn hình, cung cấp cho họ thông tin về thao tác của nút. Tuy nhiên, tính năng này cũng cung cấp sự trợ giúp tương tự cho mọi người, bạn sẽ thấy tính năng này ở chính giữa 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 với phần tử chọn không hiển thị đang được di chuột qua và chú giải công cụ theo ngữ cảnh của phần tử đó đang hiển thị.

Trang trí dòng phân cách

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

Bạn không bắt buộc phải sử dụng dòng phân cách, chỉ cần thêm một dòng phân cách cũng đã rất hiệu quả (xem ví dụ thứ ba trong video ở trên). Sau đó, tôi cung cấp từng aria-hidden="true" vì chúng chỉ để trang trí chứ 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 được đề cập tiếp theo giúp cho khoảng cách của các thuộc tính này 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

DevTools hiển thị cách căn chỉnh điều hướng breadcrumb bằng 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 trong phạm vi để con sử dụng, đồng thời thiết lập bố cục được căn chỉnh theo chiều ngang. Điều này đảm bảo rằng các đường dẫn, đườ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 chuỗi liên kết được hiển thị theo chiều dọc, căn chỉnh với lớp phủ flexbox.

Mỗi .crumb cũng thiết lập một bố cục ngang được căn chỉnh theo chiều dọc với một số khoảng trống, nhưng nhắm mục tiêu đặc biệt đến các đường liên kết con và chỉ định kiểu white-space: nowrap. Điều này rất quan trọng đối với các chuỗi breadcrumb gồm nhiều từ vì chúng ta không muốn các chuỗi này xuất hiện trên nhiều dòng. Ở phần sau của bài đăng này, chúng ta sẽ thêm kiểu để xử lý tình trạng tràn lề theo chiều ngang mà 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 của trang hiện tại nổi bật so với các đường liên kết khác. Người dùng trình đọc màn hình không chỉ có chỉ báo rõ ràng rằng đường liên kết là dành cho trang hiện tại, mà 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 bình thường có được trải nghiệm người dùng tương tự.

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

Công cụ cho nhà phát triển của lưới hiển thị phủ lên một nút mà hàng và cột đều có 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> là phần tử cuối cùng trong DOM, vì vậy, phần tử này 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, kết quả là một hộp chọn vừa vặn 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 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

Dấu vết bánh mì phải có thể đại diện cho một đường dẫn rất dài. Tôi thích cho phép các thành phần nằm ngoài màn hình theo chiều ngang khi thích hợp và tôi cảm thấy thành phần đường dẫn này đủ tiêu chuẩ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 mục bổ sung thiết lập trải nghiệm người dùng sau:

  • Cuộn ngang với vùng chứa cuộn xuống cuối cùng.
  • Khoảng đệm cuộn theo chiều ngang.
  • Một điểm chụp nhanh trên mảnh cuối cùng. Điều này có nghĩa là khi tải trang, mảnh đầu tiên sẽ tải nhanh và hiển thị.
  • Xoá điểm chụp nhanh khỏi Safari, điểm này gặp khó khăn với các tổ hợp hiệu ứng cuộn ngang và chụp nhanh.

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

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

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

Đặt cạnh nhau các đường dẫn có và không có nhãn ban đầu để so sánh.

Hỗ trợ tiếp cận

Có chuyển động

Không có nhiều chuyển động trong thành phần này, nhưng bằng cách gói chuyển đổi trong một bước 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 kiểu nào khác, hiệu ứng di chuột và lấy nét rất tuyệt vời và có ý nghĩa mà không cần transition, nhưng nếu chuyển động ổn thì 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 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, bạn cần cập nhật URL và hiển thị cho người dùng 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 nào không mong muốn xảy ra khi người dùng chỉ duyệt qua các tuỳ chọn <select>.

Hai biện pháp quan trọng đối với trải nghiệm người dùng mà JavaScript cần xử lý: Chọn đã thay đổi và muốn ngăn chặn việc kích hoạt sự kiện <select> thay đổi.

Bạn cần ngăn chặn sự kiện vội vàng do sử dụng phần tử <select>. Trên Windows Edge và có thể 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 tuỳ chọn bằng bàn phím. Đó là lý do tôi gọi phương thức này là eager (tham lam), vì người dùng chỉ chọn giả lập tuỳ chọn, 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 háo hức khiến bạn không thể sử dụng 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 qua 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 thay đổi <select> tốt 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 bàn phím trên mỗi phần tử <select> và xác định xem phím được nhấn là xác nhận điều hướng (Tab hoặc Enter) hay điều hướng không gian (ArrowUp hoặc ArrowDown). Với quyết định này, thành phần có thể quyết định chờ hoặc tiếp tục khi sự kiện cho phần tử <select> kích hoạt.

Kết luận

Giờ thì bạn đã biết cách tôi làm, còn bạn thì sao‽ 🙂

Hãy đa dạng hoá phương pháp tiếp cận và tìm hiểu tất cả các cách xây dựng ứng dụng trên web. Tạo một bản minh hoạ, tweet cho tôi các đường liên kết và tôi sẽ thêm 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