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ẻ suy nghĩ về cách tạo thành phần đường dẫn. 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ải các mẩu bánh mì sau lưng mình trong một số khu rừng tối và có thể tìm đường về nhà bằng cách lần theo các mẩu bánh mì đó.

Các đường dẫn trong bài đăng này không phải là đường dẫn tiêu chuẩn mà là đường dẫn giống như đường dẫn. 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à các 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 sẽ 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.

Trong thuật ngữ khoa học máy tính, thành phần đường dẫn này đại diện cho một mảng nhiều 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 ẩn vào trang và gói 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 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>

DevTools hiển thị một phần tử sử dụng SVG đã 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 đối với những người dùng chỉ nghe nội dung. Việc ẩn các biểu tượng này khỏi những người dùng đó sẽ giúp họ không bị làm phiền.

Đâ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 ở vị trí trung tâm 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 đã có hiệu quả (xem ví dụ thứ ba trong video ở trên). Sau đó, tôi sẽ cung cấp cho mỗi aria-hidden="true" vì chúng là nội dung 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 được đề cập tiếp theo sẽ giúp khoảng cách giữa các thành phần này trở nên đơn giản.

Kiểu

Vì màu 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 các 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 theo phạm vi để các phần tử con sử dụng, nếu không 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 đảm bảo rằng các dấu vết, đườ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 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 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;
  }}
}

Kiểu tràn lề 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 đa phương tiệ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;
  }
}

So sánh các chuỗi liên kết có và không có nhãn trang chủ.

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, 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 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>.

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

Bạn cần ngăn chặn sự kiện háo hức 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á 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 bản minh hoạ, gửi đường liên kết cho tôi trên Twitter và tôi sẽ thêm bản minh hoạ đó vào phầ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