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 một ngăn điều hướng 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 nguyên mẫu một thành phần Sidenav cho web. Tính năng này phản hồi, có trạng thái, hỗ trợ điều hướng bằng bàn phím, hoạt động có và không có JavaScript cũng như hoạt động trên các trình duyệt. Thử bản minh hoạ.

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

Tổng quan

Thậ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ố có máy tính để bàn mạnh mẽ và một số khác sẽ truy cập từ một thiết bị di động nhỏ. 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 đến thiết bị di động
Giao diện sáng và tối trên iOS và Android

Chiến thuật web

Trong dữ liệu khám phá thành phần này, tôi rất vui khi được kết hợp một số tính năng quan trọng của nền tảng web:

  1. Dịch vụ so sánh giá (CSS) :target
  2. Lưới CSS
  3. transforms CSS
  4. Truy vấn phương tiện CSS cho khung nhìn và tùy chọn người dùng
  5. JS cho focus các cải tiến về 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" là 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 cho máy tính.

Lớp giả của CSS :target

Một đường liên kết <a> sẽ đặt hàm băm URL thành #sidenav-open và đường liên kết 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>

Nhấp vào mỗi đường liên kết trong số này sẽ thay đổi trạng thái băm của URL trang, sau đó với một lớp giả (pseudo-class), tôi sẽ hiển thị và ẩn đ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 bố cục và thành phần điều hướng bên có vị trí tuyệt đối hoặc cố định. Tuy nhiên, với cú pháp grid-area, lưới 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 1 hàng và 2 cột, mỗi hàng có 1 cột được đặt tên là stack. Khi không gian bị ràng buộc, CSS sẽ chỉ định 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 có chứa thành phần điều hướng bên. Lớp này có 2 phần tử 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ệ bạn muốn cho lớp phủ trình đơn và nút đóng không gian âm của lớp phủ này.

Bản minh hoạ về điều sẽ xảy ra khi bạn thay đổi tỷ lệ.

Chuyển đổi và chuyển đổi CSS 3D

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, theo mặc định, tính năng này sẽ phủ bài viết của chúng tôi. Đây là một số trải nghiệm người dùng mà tôi sẽ chụp trong phần tiếp theo:

  • Tạo ảnh động mở và đóng
  • Chỉ tạo ảnh động có chuyển động nếu người dùng không hài lòng với điều đó
  • Tạo ảnh động visibility để tiêu điểm bàn phím không nhập 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 ưu tiên khả năng hỗ trợ tiếp cận.

Hỗ trợ người khuyết tật

Không phải ai cũng muốn có trải nghiệm chuyển động trượt ra. Trong giải pháp của chúng tôi, lựa 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 một truy vấn nội dung nghe nhìn. Giá trị truy vấn nội dung đa phương tiện này thể hiện lựa chọn chuyển động mà người dùng ưu tiên trên hệ điều hành (nếu có).

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

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

Giờ đây, khi tính năng điều hướng bên là 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 khung hiển thị ngay lập tức, duy trì trạng thái không cần 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 tính năng đ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 định vị phần tử bằng transform: translateX(-110vw).

Lưu ý là 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 điều hướng bên không hiện vào khung nhìn chính khi 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 cơ sở chính 0 và xem khi CSS trượt phần tử từ vị trí bên ngoài là -110vw sang vị trí "trong" 0 qua var(--duration) khi hàm băm URL được 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 bây giờ là ẩn trình đơn khỏi trình đọc màn hình khi trình đọc màn hình mở ra, để hệ thống không đưa tiêu điểm vào trình đơn ngoài màn hình. Tôi có thể làm việc này bằng cách thiết lập hiệu ứng 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 xuất hiện ngay lập tức để tôi có thể thấy phần tử trượt vào và nhận tiêu điểm.
  • Khi thoát, chế độ hiển thị hiệu ứng chuyển đổi sẽ bị trì hoãn để chế độ hiển thị sẽ chuyển sang hidden khi quá trình chuyển đổi kết thúc.

Các cải tiến về 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, bạn nên sử dụng phần tử <a> ở đây và phần tử này sẽ có một số tính năng hỗ trợ tiếp cận đẹp mắt miễn phí. Hãy trang trí các phần tử tương tác của chúng ta bằng nhãn nêu 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ạ nội dung lồng tiếng và trải nghiệm người dùng tương tác bằng bàn phím.

Giờ đây, các nút tương tác chính của chúng ta sẽ nêu rõ ý định sử dụng 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 kết hợp các kiểu di chuột của mình bằng cách chia sẻ chúng với tiêu điểm.

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

Rắc rối trên JavaScript

Nhấn escape để đóng

Phím Escape trên bàn phím sẽ đóng trình đơn đúng không? Hãy kết nối với dây điện thoại đó.

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 nhập vào nhật ký trình duyệt, hãy thêm JavaScript nội tuyến sau đây 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ật ký URL khi đóng, làm như thể trình đơn chưa từng mở.

Trải nghiệm người dùng tập trung

Đoạn mã tiếp theo giúp chúng ta tập trung vào các nút mở và đóng sau khi chúng mở hoặc đóng. Tôi muốn khiế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 điều hướng bên mở ra, hãy lấy tiêu điểm vào nút đóng. Khi điều hướng bên đóng, lấy tiêu điểm là 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ờ bạn đã biết tôi làm việc đó như thế nào, bạn sẽ làm thế nào?! Điều này tạo ra một số cấu trúc thành phần thú vị! Ai sẽ làm phiên bản đầu tiên với các chỗ trống? 🙂

Hãy đa dạng hoá phương pháp tiếp cận của chúng ta và tìm hiểu tất cả các cách xây dựng trên web. Tạo một video Glitch, tweet cho tôi phiên bản của bạn rồi 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