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 ngoài dạng trang trình bày 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 thành phần Sidenav cho web. Thành phần 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 với và không có JavaScript, đồng thời hoạt động trên nhiều trình duyệt. Xem bản minh hoạ.

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

Tổng quan

Thật khó khăn khi xây dựng một hệ thống điều hướng thích ứng. Một số người dùng sử dụng bàn phím, một số người sẽ sử dụng máy tính để bàn mạnh mẽ và một số người sẽ truy cập từ mộ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 đế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 quá trình 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à 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 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" từ 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 để bàn.

Lớp giả :target của 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>

Việc nhấp vào từng đường liên kết này sẽ thay đổi trạng thái băm của URL trang, sau đó với một lớp giả, tôi sẽ 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, lưới 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 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ẽ 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. Khung này có 2 phần tử con: vùng chứa điều hướng <nav> tên là [nav] và một 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ệ.

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

Bố cục của chúng ta hiện được xếp chồng theo 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, nội dung đó sẽ chồng lên bài viết của chúng tôi theo mặc định. Dưới đây là một số trải nghiệm người dùng mà tôi sẽ nhắm đến trong phần tiếp theo này:

  • Tạo ảnh động cho thao tác mở và đóng
  • Chỉ tạo ảnh động bằng chuyển động nếu người dùng đồng ý với điều đó
  • Tạo ảnh động cho visibility để tiêu điểm bằng 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 hoạt ảnh chuyển động, tôi muốn ưu tiên khả năng hỗ trợ tiếp cận.

Xe có hỗ trợ tiếp cận

Không phải ai cũng muốn 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 truy vấn nội dung đa phương tiệ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ạ hoạt động tương tác có áp dụng và không áp dụng thời lượng.

Khi thanh điều hướng bên ở trạng thái trượt mở và đóng, nếu người dùng muốn giảm chuyển động, tôi sẽ ngay lập tức di chuyển phần tử vào khung hiển thị, duy trì trạng thái không chuyển động.

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

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

Để đặt trạng thái mặc định của đ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 điều hướng bên không xuất hiện trong 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 giá trị :target, hãy đặt vị trí translateX() thành homebase 0 và xem khi CSS trượt phần tử từ vị trí ra ngoài -110vw sang vị trí "ở" 0 trên 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 hiện tại là ẩn trình đơn khỏi trình đọc màn hình khi trình đơn đó được mở ra để các hệ thống không đưa 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 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 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 chuyển ra ngoài, chế độ hiển thị hiệu ứng chuyển đổi sẽ hiển thị nhưng trì hoãn để chuyển sang hidden ở cuối 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 trên 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 tương tác qua bàn phím và lồng tiếng.

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 vào đó.

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ý duyệt web, hãy thêm nội tuyến JavaScript 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, khiến trình đơn như thể chưa từng mở trình đơn.

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ập trung vào các nút mở và đóng sau khi các nút này mở hoặc đóng. Tôi muốn bật/tắt một cách 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 điều hướng bên đóng, hãy lấy 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ờ bạn đã biết cách tôi thực hiện điều đó, 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ẽ 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 tiếp cận của chúng tôi 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 phối lại, tweet cho tôi phiên bản của bạn, và tôi sẽ thêm nó vào phần Bản phối lại trong cộng đồng ở bên dưới.

Bản phối lại của cộng đồng