Xây dựng thành phần điều hướng bên

Tổng quan cơ bản về cách tạo thanh điều hướng bên trượt ra thích ứng

Trong bài đăng này, tôi muốn chia sẻ với bạn cách tôi tạo bản minh hoạ cho một thành phần Sidenav (thanh điều hướng bên) cho web. Thành phần này có khả năng thích ứng, có trạng thái, hỗ trợ thao tác điều hướng bằng bàn phím, hoạt động có và không có JavaScript, đồng thời hoạt động trên nhiều trình duyệt. Hãy dùng thử 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

Rất khó để xây dựng một hệ thống điều hướng thích ứng. Một số người dùng sẽ sử dụng bàn phím, một số người dùng sẽ có máy tính để bàn mạnh mẽ và một số người dùng sẽ truy cập từ thiết bị di động nhỏ. Tất cả những người truy cập đều có thể mở và đóng trình đơn.

Bản minh hoạ bố cục thích ứng từ máy tính sang thiết bị di động
Giao diện sáng và tối trên iOS và Android

Chiến thuật web

Trong quá trình khám phá thành phần này, tôi rất vui khi có thể kết hợp một số tính năng quan trọng của nền tảng web:

  1. CSS :target
  2. Lưới CSS
  3. Chuyển đổi CSS
  4. Truy vấn nội dung đa phương tiện CSS cho khung nhìn và lựa chọn ưu tiên của người dùng
  5. JS cho focus Các tính năng nâng cao trải nghiệm người dùng

Giải pháp của tôi có một thanh bên và chỉ bật/tắt khi ở khung nhìn "thiết bị di động" có kích thước 540px trở xuống. 540px sẽ là điểm ngắt để chuyển đổi giữa bố cục tương tác trên thiết bị di động và bố cục tĩnh trên máy tính.

Lớp giả lập :target CSS

Một đường liên kết <a> sẽ đặt hàm băm URL thành #sidenav-open và thành phần còn lại thành trống (''). Cuối cùng, một phần tử có id để khớp với hàm băm:

<a href="#sidenav-open" id="sidenav-button" title="Open Menu" aria-label="Open Menu">

<a href="#" id="sidenav-close" title="Close Menu" aria-label="Close Menu"></a>

<aside id="sidenav-open">
  …
</aside>

Khi nhấp vào từng đường liên kết này, trạng thái băm của URL trang sẽ thay đổi, sau đó với một lớp giả, tôi hiển thị và ẩn thanh điều hướng bên:

@media (max-width: 540px) {
  #sidenav-open {
    visibility: hidden;
  }

  #sidenav-open:target {
    visibility: visible;
  }
}

Lưới CSS

Trước đây, tôi chỉ sử dụng các thành phần và bố cục sidenav theo vị trí tuyệt đối hoặc cố định. Tuy nhiên, với cú pháp grid-area, Grid cho phép chúng ta gán nhiều phần tử cho cùng một hàng hoặc cột.

Ngăn xếp

Phần tử bố cục chính #sidenav-container là một lưới tạo ra 1 hàng và 2 cột, trong đó mỗi hàng và cột có tên là stack. Khi không gian bị hạn chế, CSS sẽ gán tất cả phần tử con của phần tử <main> cho cùng một tên lưới, đặt tất cả các phần tử vào cùng một không gian, tạo ra một ngăn xếp.

#sidenav-container {
  display: grid;
  grid: [stack] 1fr / min-content [stack] 1fr;
  min-height: 100vh;
}

@media (max-width: 540px) {
  #sidenav-container > * {
    grid-area: stack;
  }
}

<aside> là phần tử ảnh động chứa thanh điều hướng bên. Thành phần này có 2 thành phần con: vùng chứa điều hướng <nav> tên là [nav] và phông nền <a> tên là [escape], dùng để đóng trình đơn.

#sidenav-open {
  display: grid;
  grid-template-columns: [nav] 2fr [escape] 1fr;
}

Điều chỉnh 2fr1fr để tìm tỷ lệ mà bạn muốn cho lớp phủ trình đơn và nút đóng không gian âm của lớp phủ đó.

Bản minh hoạ những gì xảy ra khi bạn thay đổi tỷ lệ.

Hiệu ứng chuyển đổi và biến đổi 3D CSS

Bố cục của chúng ta hiện được xếp chồng ở kích thước khung nhìn trên thiết bị di động. Cho đến khi tôi thêm một số kiểu mới, lớp phủ này sẽ phủ lên bài viết theo mặc định. Sau đây là một số trải nghiệm người dùng mà tôi hướng đến trong phần tiếp theo:

  • Tạo ảnh động mở và đóng
  • Chỉ tạo ảnh động bằng chuyển động nếu người dùng đồng ý
  • Tạo ảnh động cho visibility để tiêu điểm bàn phím không đi vào phần tử ngoài màn hình

Khi bắt đầu triển khai ảnh động chuyển động, tôi muốn bắt đầu bằng khả năng hỗ trợ tiếp cận.

Chuyển động hỗ trợ tiếp cận

Không phải ai cũng muốn trải nghiệm thao tác trượt ra. Trong giải pháp của chúng tôi, tuỳ chọn ưu tiên này được áp dụng bằng cách điều chỉnh biến CSS --duration bên trong truy vấn nội dung nghe nhìn. Giá trị truy vấn nội dung nghe nhìn này thể hiện lựa chọn ưu tiên của người dùng về hệ điều hành đối với chuyển động (nếu có).

#sidenav-open {
  --duration: .6s;
}

@media (prefers-reduced-motion: reduce) {
  #sidenav-open {
    --duration: 1ms;
  }
}
Bản minh hoạ về lượt tương tác có và không áp dụng thời lượng.

Bây giờ, khi thanh điều hướng bên của chúng ta trượt mở và đóng, nếu người dùng muốn giảm chuyển động, tôi sẽ di chuyển phần tử đó vào chế độ xem ngay lập tức, duy trì trạng thái mà không có chuyển động.

Chuyển đổi, biến đổi, dịch

Sidenav out (mặc định)

Để đặt trạng thái mặc định của thanh điều hướng bên trên thiết bị di động thành trạng thái ngoài màn hình, tôi sẽ định vị phần tử bằng transform: translateX(-110vw).

Lưu ý: Tôi đã thêm một 10vw khác vào mã ngoài màn hình thông thường của -100vw, để đảm bảo box-shadow của thanh điều hướng bên không xem trộm vào khung nhìn chính khi thanh điều hướng bên bị ẩn.

@media (max-width: 540px) {
  #sidenav-open {
    visibility: hidden;
    transform: translateX(-110vw);
    will-change: transform;
    transition:
      transform var(--duration) var(--easeOutExpo),
      visibility 0s linear var(--duration);
  }
}
Điều hướng bên trong

Khi phần tử #sidenav khớp với :target, hãy đặt vị trí translateX() thành homebase 0, và xem CSS trượt phần tử từ vị trí ngoài -110vw, sang vị trí "trong" 0 trên var(--duration) khi hàm băm URL thay đổi.

@media (max-width: 540px) {
  #sidenav-open:target {
    visibility: visible;
    transform: translateX(0);
    transition:
      transform var(--duration) var(--easeOutExpo);
  }
}

Chế độ hiển thị chuyển đổi

Mục tiêu hiện tại là ẩn trình đơn khỏi trình đọc màn hình khi trình đơn đó ẩn, để hệ thống không đặt tiêu điểm vào trình đơn ngoài màn hình. Tôi thực hiện việc này bằng cách đặt chuyển đổi chế độ hiển thị khi :target thay đổi.

  • Khi đi vào, đừng chuyển đổi chế độ hiển thị; hãy hiển thị ngay để tôi có thể thấy phần tử trượt vào và chấp nhận tiêu điểm.
  • Khi thoát, hãy chuyển đổi chế độ hiển thị nhưng trì hoãn chế độ này để chuyển sang hidden khi kết thúc quá trình chuyển đổi.

Các tính năng nâng cao cho trải nghiệm người dùng hỗ trợ tiếp cận

Giải pháp này dựa vào việc thay đổi URL để quản lý trạng thái. Đương nhiên là bạn nên sử dụng phần tử <a> ở đây và phần tử này sẽ nhận được một số tính năng hỗ trợ tiếp cận hữu ích miễn phí. Hãy tô điểm cho các thành phần tương tác của chúng ta bằng các nhãn thể hiện rõ ý định.

<a href="#" id="sidenav-close" title="Close Menu" aria-label="Close Menu"></a>

<a href="#sidenav-open" id="sidenav-button" class="hamburger" title="Open Menu" aria-label="Open Menu">
  <svg>...</svg>
</a>
Bản minh hoạ trải nghiệm người dùng về giọng lồng tiếng và tương tác bằng bàn phím.

Giờ đây, các nút tương tác chính đã nêu rõ ý định của chúng đối với cả chuột và bàn phím.

:is(:hover, :focus)

Bộ chọn giả chức năng CSS tiện dụng này cho phép chúng tôi nhanh chóng bao gồm các kiểu di chuột bằng cách chia sẻ chúng với tiêu điểm.

.hamburger:is(:hover, :focus) svg > line {
  stroke: hsl(var(--brandHSL));
}

Thêm JavaScript

Nhấn escape để đóng

Phím Escape trên bàn phím sẽ đóng trình đơn phải không? Hãy kết nối các phần đó.

const sidenav = document.querySelector('#sidenav-open');

sidenav.addEventListener('keyup', event => {
  if (event.code === 'Escape') document.location.hash = '';
});
Nhật ký duyệt web

Để ngăn hoạt động tương tác mở và đóng xếp chồng nhiều mục vào nhật ký duyệt web, hãy thêm JavaScript nội tuyến sau vào nút đóng:

<a href="#" id="sidenav-close" title="Close Menu" aria-label="Close Menu" onchange="history.go(-1)"></a>

Thao tác này sẽ xoá mục nhập nhật ký URL khi đóng, khiến trình đơn giống như chưa bao giờ được mở.

Tập trung vào trải nghiệm người dùng

Đoạn mã tiếp theo giúp chúng ta đặt tiêu điểm vào các nút mở và đóng sau khi chúng mở hoặc đóng. Tôi muốn việc bật/tắt trở nên dễ dàng.

sidenav.addEventListener('transitionend', e => {
  const isOpen = document.location.hash === '#sidenav-open';

  isOpen
      ? document.querySelector('#sidenav-close').focus()
      : document.querySelector('#sidenav-button').focus();
})

Khi bảng điều hướng bên mở ra, hãy lấy tiêu điểm vào nút đóng. Khi thanh điều hướng bên đóng, hãy đặt tiêu điểm vào nút mở. Tôi thực hiện việc này bằng cách gọi focus() trên phần tử trong JavaScript.

Kết luận

Giờ thì bạn đã biết cách tôi làm, còn bạn thì sao?! Điều này tạo ra một số cấu trúc thành phần thú vị! Ai sẽ thiết kế phiên bản đầu tiên có khung giờ nhận đặt quảng cá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 Glitch, rồi twitt cho tôi phiên bản của bạn. Tôi sẽ thêm phiên bản đó 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