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 sidenav 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 mẫu một thành phần Sidenav cho web có khả năng thích ứng, có trạng thái, hỗ trợ thao tác 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. Dùng thử 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

Việc xây dựng một hệ thống điều hướng thích ứng là một việc khó khăn. 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ừ một thiết bị di động nhỏ. Mọi 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
Chủ đề sáng và tối trên iOS và Android

Web Tactics

Trong quá trình khám phá thành phần này, tôi đã có cơ hội 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. Biến đổi CSS
  4. Truy vấn phương tiện CSS cho khung hiển thị và lựa chọn ưu tiên của người dùng
  5. JS cho focus Các điểm 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 hiển thị "thiết bị di động" có kích thước 540px trở xuống. 540px sẽ là điểm ngắt của chúng ta để 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ả :target CSS

Một đường liên kết <a> đặ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>

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 đó tôi sẽ hiện và ẩn sidenav bằng một lớp giả:

@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 sidenav có vị trí tuyệt đối hoặc cố định. Tuy nhiên, lưới có cú pháp grid-area cho phép chúng ta chỉ định 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, mỗi cột có tên là stack. Khi không gian bị hạn chế, CSS sẽ chỉ định tất cả cá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ử tạo ảnh động chứa chế độ đ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> có tên là [nav] và một phông nền <a> có 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 khoảng trống của lớp phủ đó.

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

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

Giờ đây, bố cục của chúng ta được xếp chồng lên nhau ở kích thước khung hiển thị 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, phần này sẽ phủ lên bài viết của chúng ta. Sau đây là một số trải nghiệm người dùng mà tôi muốn hướng đến trong phần tiếp theo:

  • Tạo hiệu ứng mở và đóng
  • Chỉ tạo ảnh động khi người dùng đồng ý
  • Tạo hiệu ứng độ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 cách ưu tiên 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 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 một 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 nghe nhìn này thể hiện lựa chọn ưu tiên của người dùng về chuyển động trong 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ạ về lượt tương tác có và không áp dụng thời lượng.

Giờ đây, khi sidenav của chúng tôi đang trượt mở và đóng, nếu người dùng muốn giảm chuyển động, tôi sẽ di chuyển ngay phần tử vào chế độ xem, duy trì trạng thái mà không có chuyển động.

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

Thanh điều hướng bên (mặc định)

Để đặt trạng thái mặc định của sidenav trên thiết bị di động thành trạng thái ngoài màn hình, tôi đặt vị trí của 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 sidenav không xuất hiện trong khung hiển thị 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);
  }
}
Thanh đ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 0 homebase và xem CSS trượt phần tử từ vị trí -110vw ra ngoài đến vị trí "vào trong" 0 trong 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ị hiệu ứng 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 không xuất hiện, để hệ thống không đặt tiêu điểm vào một trình đơn ngoài màn hình. Tôi thực hiện việc này bằng cách đặt 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 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 đi ra ngoài, hãy chuyển đổi chế độ hiển thị nhưng trì hoãn để chế độ này chuyển sang hidden ở cuối quá trình chuyển đổi.

Cải thiện trải nghiệm người dùng về khả nă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ó sẵn một số tính năng hỗ trợ tiếp cận hữu ích. Hãy trang trí các phần tử tương tác bằng những 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 khi tương tác bằng giọng nói và bàn phím.

Giờ đây, các nút tương tác chính của chúng tôi 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 giúp chúng ta nhanh chóng bao gồm cả 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, đúng không? Hãy kết nối các thiết bị đó.

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 chặn tương tác mở và đóng xếp chồng nhiều mục vào nhật ký trình duyệt, 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 URL trong nhật ký khi đóng, khiến cho trình đơn như thể chưa từng được mở.

Trải nghiệm người dùng khi 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 việc chuyển đổi 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 sidenav mở ra, hãy tập trung vào nút đóng. Khi sidenav đóng, hãy tập trung 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ờ bạn đã biết cách tôi làm, vậy bạn sẽ làm như thế nào?! Điều này tạo nên một cấu trúc thành phần thú vị! Ai sẽ tạo phiên bản đầu tiên có các vị trí? 🙂

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. Tạo một Glitch, gửi cho tôi một tweet về phiên bản của bạn và 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