Xây dựng thành phần Thẻ

Tổng quan cơ bản về cách tạo một thành phần thẻ tương tự như các thành phần có trong ứng dụng iOS và Android.

Trong bài đăng này, tôi muốn chia sẻ suy nghĩ về việc xây dựng một thành phần Thẻ cho web có khả năng thích ứng, hỗ trợ nhiều thiết bị đầu vào và hoạt động trên nhiều trình duyệt. Dùng thử bản minh hoạ.

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

Thẻ là một thành phần phổ biến của hệ thống thiết kế nhưng có thể có nhiều hình dạng và hình thức. Trước đây, chúng tôi có các thẻ trên máy tính được tạo dựa trên phần tử <frame>, còn hiện tại, chúng tôi có các thành phần mượt mà trên thiết bị di động giúp tạo hiệu ứng cho nội dung dựa trên các thuộc tính vật lý. Tất cả đều đang cố gắng làm cùng một việc: tiết kiệm dung lượng.

Ngày nay, những yếu tố cần thiết của trải nghiệm người dùng với thẻ là một vùng điều hướng bằng nút, giúp bật/tắt khả năng hiển thị nội dung trong một khung hiển thị. Nhiều vùng nội dung khác nhau dùng chung một không gian, nhưng được trình bày có điều kiện dựa trên nút được chọn trong thanh điều hướng.

ảnh ghép khá hỗn loạn do sự đa dạng về phong cách mà web đã áp dụng cho khái niệm thành phần
Ảnh ghép về các kiểu thiết kế web của thành phần thẻ trong hơn 10 năm qua

Web Tactics

Nhìn chung, tôi thấy thành phần này khá dễ xây dựng nhờ một số tính năng quan trọng của nền tảng web:

  • scroll-snap-points để có các thao tác vuốt và tương tác bàn phím mượt mà với các vị trí dừng cuộn thích hợp
  • Đường liên kết sâu thông qua hàm băm URL cho trình duyệt xử lý tính năng neo cuộn trong trang và hỗ trợ chia sẻ
  • Hỗ trợ trình đọc màn hình bằng cách đánh dấu phần tử <a>id="#hash"
  • prefers-reduced-motion để bật hiệu ứng chuyển đổi mờ dần và tính năng cuộn tức thì trong trang
  • Tính năng @scroll-timeline trên web đang ở trạng thái nháp để tự động gạch chân và thay đổi màu thẻ đã chọn

HTML

Về cơ bản, UX ở đây là: nhấp vào một đường liên kết, để URL biểu thị trạng thái trang lồng nhau, sau đó xem nội dung cập nhật khi trình duyệt cuộn đến phần tử phù hợp.

Có một số thành phần nội dung có cấu trúc trong đó: đường liên kết và :target. Chúng ta cần một danh sách các đường liên kết (<nav> là lựa chọn phù hợp) và một danh sách các phần tử <article> (<section> là lựa chọn phù hợp). Mỗi hàm băm đường liên kết sẽ khớp với một phần, cho phép trình duyệt cuộn mọi thứ thông qua việc neo.

Người dùng nhấp vào một nút liên kết, nội dung được lấy tiêu điểm sẽ trượt vào

Ví dụ: khi bạn nhấp vào một đường liên kết, bài viết :target sẽ tự động được lấy làm tiêu điểm trong Chrome 89 mà không cần JS. Sau đó, người dùng có thể cuộn nội dung bài viết bằng thiết bị đầu vào như bình thường. Đây là nội dung bổ sung, như được chỉ ra trong mã đánh dấu.

Tôi đã sử dụng mã đánh dấu sau đây để sắp xếp các thẻ:

<snap-tabs>
  <header>
    <nav>
      <a></a>
      <a></a>
      <a></a>
      <a></a>
    </nav>
  </header>
  <section>
    <article></article>
    <article></article>
    <article></article>
    <article></article>
  </section>
</snap-tabs>

Tôi có thể thiết lập mối liên kết giữa các phần tử <a><article> bằng các thuộc tính hrefid như sau:

<snap-tabs>
  <header>
    <nav>
      <a href="#responsive"></a>
      <a href="#accessible"></a>
      <a href="#overscroll"></a>
      <a href="#more"></a>
    </nav>
  </header>
  <section>
    <article id="responsive"></article>
    <article id="accessible"></article>
    <article id="overscroll"></article>
    <article id="more"></article>
  </section>
</snap-tabs>

Tiếp theo, tôi điền vào các bài viết một lượng lorem hỗn hợp và các đường liên kết có độ dài hỗn hợp và bộ tiêu đề hình ảnh. Khi có nội dung để làm việc, chúng ta có thể bắt đầu bố cục.

Bố cục có thể cuộn

Có 3 loại vùng có thể cuộn trong thành phần này:

  • Thanh điều hướng (màu hồng) có thể cuộn theo chiều ngang
  • Khu vực nội dung (màu xanh dương) có thể cuộn theo chiều ngang
  • Mỗi mục bài viết (màu xanh lục) đều có thể cuộn theo chiều dọc.
3 hộp đầy màu sắc có mũi tên chỉ hướng trùng khớp với màu sắc, phác thảo các vùng có thể cuộn và cho biết hướng cuộn.

Có 2 loại phần tử liên quan đến thao tác cuộn:

  1. Một cửa sổ
    Một hộp có kích thước xác định có kiểu thuộc tính overflow.
  2. Một vùng hiển thị có kích thước quá lớn
    Trong bố cục này, đó là các vùng chứa danh sách: đường liên kết điều hướng, bài viết trong phần và nội dung bài viết.

Bố cục <snap-tabs>

Bố cục cấp cao nhất mà tôi chọn là flex (Flexbox). Tôi đặt hướng thành column, nên tiêu đề và phần được sắp xếp theo chiều dọc. Đây là cửa sổ cuộn đầu tiên của chúng ta và cửa sổ này sẽ ẩn mọi thứ bằng thuộc tính overflow: hidden. Tiêu đề và phần sẽ sớm sử dụng tính năng cuộn quá mức dưới dạng các vùng riêng lẻ.

HTML
<snap-tabs>
  <header></header>
  <section></section>
</snap-tabs>
CSS
  snap-tabs {
  display: flex;
  flex-direction: column;

  /* establish primary containing box */
  overflow: hidden;
  position: relative;

  & > section {
    /* be pushy about consuming all space */
    block-size: 100%;
  }

  & > header {
    /* defend against 
needing 100% */ flex-shrink: 0; /* fixes cross browser quarks */ min-block-size: fit-content; } }

Quay lại sơ đồ 3 cuộn đầy màu sắc:

  • <header> hiện đã được chuẩn bị để trở thành vùng chứa cuộn (màu hồng).
  • <section> được chuẩn bị để trở thành vùng chứa cuộn (màu xanh dương).

Các khung hình mà tôi đã đánh dấu bên dưới bằng VisBug giúp chúng ta thấy các cửa sổ mà vùng chứa có thể cuộn đã tạo.

các phần tử tiêu đề và phần tử mục có lớp phủ màu hồng cánh sen, phác thảo khoảng trống mà chúng chiếm trong thành phần

Bố cục <header> thẻ

Bố cục tiếp theo gần giống như vậy: Tôi sử dụng flex để tạo thứ tự dọc.

HTML
<snap-tabs>
  <header>
    <nav></nav>
    <span class="snap-indicator"></span>
  </header>
  <section></section>
</snap-tabs>
CSS
header {
  display: flex;
  flex-direction: column;
}

.snap-indicator phải di chuyển theo chiều ngang cùng với nhóm đường liên kết và bố cục tiêu đề này sẽ giúp thiết lập giai đoạn đó. Không có phần tử nào có vị trí tuyệt đối ở đây!

các phần tử nav và span.indicator có lớp phủ màu hồng cánh sen, phác thảo không gian mà chúng chiếm trong thành phần

Tiếp theo là kiểu cuộn. Hoá ra chúng ta có thể chia sẻ các kiểu cuộn giữa 2 vùng cuộn ngang (tiêu đề và phần), vì vậy, tôi đã tạo một lớp tiện ích .scroll-snap-x.

.scroll-snap-x {
  /* browser decide if x is ok to scroll and show bars on, y hidden */
  overflow: auto hidden;
  /* prevent scroll chaining on x scroll */
  overscroll-behavior-x: contain;
  /* scrolling should snap children on x */
  scroll-snap-type: x mandatory;

  @media (hover: none) {
    scrollbar-width: none;

    &::-webkit-scrollbar {
      width: 0;
      height: 0;
    }
  }
}

Mỗi thành phần đều cần có nội dung tràn trên trục x, tính năng ngăn cuộn để chặn thao tác cuộn quá mức, thanh cuộn ẩn cho thiết bị cảm ứng và cuối cùng là tính năng scroll-snap để khoá các vùng trình bày nội dung. Thứ tự thẻ bàn phím của chúng tôi có thể truy cập và mọi hoạt động tương tác đều hướng dẫn tiêu điểm một cách tự nhiên. Các vùng chứa cuộn nhanh cũng có được một kiểu băng chuyền tương tác đẹp mắt từ bàn phím.

Bố cục tiêu đề <nav> của thẻ

Các đường liên kết điều hướng cần được bố trí theo một dòng, không có dấu ngắt dòng, được căn giữa theo chiều dọc và mỗi mục liên kết phải khớp với vùng chứa scroll-snap. Swift work for 2021 CSS!

HTML
<nav>
  <a></a>
  <a></a>
  <a></a>
  <a></a>
</nav>
CSS
  nav {
  display: flex;

  & a {
    scroll-snap-align: start;

    display: inline-flex;
    align-items: center;
    white-space: nowrap;
  }
}

Mỗi đường liên kết đều có kiểu và kích thước riêng, vì vậy, bố cục điều hướng chỉ cần chỉ định hướng và luồng. Chiều rộng riêng biệt của các mục điều hướng giúp quá trình chuyển đổi giữa các thẻ trở nên thú vị khi chỉ báo điều chỉnh chiều rộng theo mục tiêu mới. Tuỳ thuộc vào số lượng phần tử trong đây, trình duyệt sẽ hiển thị hoặc không hiển thị một thanh cuộn.

các phần tử a của nav có lớp phủ màu hồng tươi, phác thảo không gian mà chúng chiếm trong thành phần cũng như vị trí chúng tràn ra

Bố cục <section> thẻ

Phần này là một mục linh hoạt và cần phải là đối tượng sử dụng không gian chính. Bạn cũng cần tạo các cột để đặt bài viết vào. Một lần nữa, xin chúc mừng CSS 2021 đã hoàn thành công việc một cách nhanh chóng! block-size: 100% kéo dài phần tử này để lấp đầy phần tử mẹ nhiều nhất có thể, sau đó đối với bố cục riêng, phần tử này sẽ tạo một loạt cột có chiều rộng bằng 100% chiều rộng của phần tử mẹ. Tỷ lệ phần trăm hoạt động hiệu quả ở đây vì chúng ta đã viết các ràng buộc chặt chẽ cho thành phần mẹ.

HTML
<section>
  <article></article>
  <article></article>
  <article></article>
  <article></article>
</section>
CSS
  section {
  block-size: 100%;

  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 100%;
}

Như thể chúng ta đang nói "mở rộng theo chiều dọc nhiều nhất có thể, theo cách ép buộc" (hãy nhớ tiêu đề mà chúng ta đã đặt thành flex-shrink: 0: đây là cách chống lại việc mở rộng ép buộc này), việc này sẽ đặt chiều cao hàng cho một nhóm cột có chiều cao đầy đủ. Kiểu auto-flow cho biết lưới luôn bố trí các thành phần con theo một đường ngang, không bao bọc, đúng như chúng ta muốn; để tràn cửa sổ mẹ.

các phần tử bài viết có lớp phủ màu hồng tươi, phác thảo không gian mà chúng chiếm trong thành phần và nơi chúng tràn

Đôi khi tôi thấy những điều này thật khó hiểu! Phần tử này vừa nằm trong một hộp, vừa tạo ra một nhóm hộp. Tôi hy vọng hình ảnh và phần giải thích sẽ giúp ích cho bạn.

Bố cục <article> thẻ

Người dùng phải có thể cuộn nội dung bài viết và thanh cuộn chỉ xuất hiện nếu có nội dung tràn. Các phần tử bài viết này nằm ở vị trí gọn gàng. Chúng vừa là thành phần mẹ có thể cuộn vừa là thành phần con có thể cuộn. Trình duyệt thực sự đang xử lý một số thao tác tương tác phức tạp bằng cách chạm, chuột và bàn phím cho chúng ta ở đây.

HTML
<article>
  <h2></h2>
  <p></p>
  <p></p>
  <h2></h2>
  <p></p>
  <p></p>
  ...
</article>
CSS
article {
  scroll-snap-align: start;

  overflow-y: auto;
  overscroll-behavior-y: contain;
}

Tôi chọn để các bài viết được căn chỉnh trong trình cuộn mẹ. Tôi rất thích cách các mục liên kết điều hướng và các phần tử bài viết gắn vào phần đầu của vùng chứa cuộn tương ứng. Mối quan hệ này có vẻ hài hoà.

phần tử bài viết và các phần tử con của phần tử này có lớp phủ màu hồng tươi, phác thảo khoảng trống mà chúng chiếm trong thành phần và hướng chúng tràn

Bài viết này là một thành phần con của lưới và kích thước của nó được xác định trước là vùng khung nhìn mà chúng ta muốn cung cấp trải nghiệm người dùng khi di chuyển. Điều này có nghĩa là tôi không cần bất kỳ kiểu chiều cao hoặc chiều rộng nào ở đây, tôi chỉ cần xác định cách nó tràn. Tôi đặt overflow-y thành auto, sau đó cũng chặn các thao tác cuộn bằng thuộc tính overscroll-behavior tiện dụng.

Tóm tắt 3 khu vực có thể cuộn

Bên dưới, tôi đã chọn "luôn hiện thanh cuộn" trong phần cài đặt hệ thống. Tôi nghĩ điều quan trọng gấp đôi là bố cục phải hoạt động khi bật chế độ cài đặt này, vì tôi cần xem xét bố cục và chế độ điều phối thao tác cuộn.

3 thanh cuộn được đặt để hiển thị, hiện đang chiếm không gian bố cục và thành phần của chúng ta vẫn trông rất đẹp

Tôi nghĩ rằng việc nhìn thấy rãnh thanh cuộn trong thành phần này sẽ giúp bạn thấy rõ các vùng có thể cuộn, hướng mà chúng hỗ trợ và cách chúng tương tác với nhau. Hãy cân nhắc cách mỗi khung cửa sổ cuộn này cũng là thành phần mẹ linh hoạt hoặc thành phần mẹ lưới đối với một bố cục.

Công cụ cho nhà phát triển có thể giúp chúng ta hình dung điều này:

các vùng có thể cuộn có lớp phủ công cụ lưới và flexbox, phác thảo không gian mà chúng chiếm trong thành phần và hướng mà chúng tràn
Chromium Devtools, cho thấy bố cục phần tử điều hướng flexbox chứa đầy các phần tử neo, bố cục phần lưới chứa đầy các phần tử bài viết và các phần tử bài viết chứa đầy các đoạn văn và một phần tử tiêu đề.

Bố cục cuộn đã hoàn tất: có thể di chuyển nhanh, có thể liên kết sâu và có thể truy cập bằng bàn phím. Nền tảng vững chắc để nâng cao trải nghiệm người dùng, phong cách và sự hài lòng.

Điểm nổi bật của tính năng

Các thành phần con được cuộn nhanh vẫn giữ nguyên vị trí cố định trong quá trình thay đổi kích thước. Điều này có nghĩa là JavaScript sẽ không cần đưa bất kỳ nội dung nào vào chế độ xem khi xoay thiết bị hoặc thay đổi kích thước trình duyệt. Hãy thử chế độ Thiết bị trong Công cụ cho nhà phát triển của Chromium bằng cách chọn một chế độ bất kỳ khác với chế độ Thích ứng, rồi thay đổi kích thước khung thiết bị. Lưu ý rằng phần tử này vẫn hiển thị và được khoá cùng với nội dung của nó. Tính năng này đã có sẵn kể từ khi Chromium cập nhật quá trình triển khai để phù hợp với quy cách. Dưới đây là bài đăng trên blog về tính năng này.

Hoạt ảnh

Mục tiêu của công việc tạo ảnh động ở đây là liên kết rõ ràng các lượt tương tác với thông tin phản hồi của giao diện người dùng. Điều này giúp hướng dẫn hoặc hỗ trợ người dùng khám phá tất cả nội dung một cách liền mạch (hy vọng là vậy). Tôi sẽ thêm chuyển động có mục đích và có điều kiện. Giờ đây, người dùng có thể chỉ định lựa chọn ưu tiên về chuyển động trong hệ điều hành của họ và tôi rất vui khi đáp ứng các lựa chọn ưu tiên đó trong giao diện của mình.

Tôi sẽ liên kết một đường kẻ chân thẻ với vị trí cuộn của bài viết. Tính năng căn chỉnh nhanh không chỉ giúp căn chỉnh đẹp mắt mà còn giúp cố định điểm bắt đầu và kết thúc của một ảnh động. Điều này giúp <nav> (đóng vai trò như một bản đồ thu nhỏ) kết nối với nội dung. Chúng ta sẽ kiểm tra lựa chọn ưu tiên về chuyển động của người dùng từ cả CSS và JS. Có một vài nơi tuyệt vời để bạn thể hiện sự chu đáo!

Hành vi cuộn

Bạn có thể cải thiện hành vi chuyển động của cả :targetelement.scrollIntoView(). Theo mặc định, thời gian này là tức thì. Trình duyệt chỉ đặt vị trí cuộn. Vậy nếu chúng ta muốn chuyển sang vị trí cuộn đó thay vì nhấp nháy thì sao?

@media (prefers-reduced-motion: no-preference) {
  .scroll-snap-x {
    scroll-behavior: smooth;
  }
}

Vì chúng ta đang giới thiệu chuyển động ở đây và chuyển động mà người dùng không kiểm soát (chẳng hạn như thao tác cuộn), nên chúng ta chỉ áp dụng kiểu này nếu người dùng không có lựa chọn ưu tiên nào trong hệ điều hành của họ về việc giảm chuyển động. Bằng cách này, chúng tôi chỉ giới thiệu chuyển động cuộn cho những người chấp nhận tính năng này.

Chỉ báo thẻ

Mục đích của ảnh động này là giúp liên kết chỉ báo với trạng thái của nội dung. Tôi quyết định sử dụng các kiểu chuyển đổi màu border-bottom cho những người dùng thích chế độ giảm chuyển động và ảnh động trượt liên kết với thao tác cuộn + chuyển đổi màu cho những người dùng không ngại chuyển động.

Trong Chromium DevTools, tôi có thể bật/tắt lựa chọn ưu tiên và minh hoạ 2 kiểu chuyển đổi khác nhau. Tôi đã rất vui khi xây dựng ứng dụng này.

@media (prefers-reduced-motion: reduce) {
  snap-tabs > header a {
    border-block-end: var(--indicator-size) solid hsl(var(--accent) / 0%);
    transition: color .7s ease, border-color .5s ease;

    &:is(:target,:active,[active]) {
      color: var(--text-active-color);
      border-block-end-color: hsl(var(--accent));
    }
  }

  snap-tabs .snap-indicator {
    visibility: hidden;
  }
}

Tôi ẩn .snap-indicator khi người dùng muốn giảm chuyển động vì tôi không cần đến nó nữa. Sau đó, tôi thay thế bằng các kiểu border-block-endtransition. Ngoài ra, hãy lưu ý trong phần tương tác với các thẻ rằng mục điều hướng đang hoạt động không chỉ có một đường gạch chân nổi bật của thương hiệu mà màu văn bản cũng tối hơn. Phần tử đang hoạt động có độ tương phản màu văn bản cao hơn và điểm nhấn là ánh sáng dưới nền sáng.

Chỉ cần thêm vài dòng CSS, người dùng sẽ cảm thấy được quan tâm (theo nghĩa là chúng ta đang tôn trọng một cách chu đáo các lựa chọn ưu tiên của họ về chuyển động). Tôi thích cái tên đó.

@scroll-timeline

Trong phần trên, tôi đã cho bạn thấy cách tôi xử lý các kiểu hiệu ứng làm mờ chuyển tiếp khi giảm chuyển động và trong phần này, tôi sẽ cho bạn thấy cách tôi liên kết chỉ báo và vùng cuộn với nhau. Tiếp theo là một số nội dung thử nghiệm thú vị. Tôi hy vọng bạn cũng háo hức như tôi.

const { matches:motionOK } = window.matchMedia(
  '(prefers-reduced-motion: no-preference)'
);

Trước tiên, tôi kiểm tra lựa chọn ưu tiên về chuyển động của người dùng bằng JavaScript. Nếu kết quả của thao tác này là false, tức là người dùng muốn giảm chuyển động, thì chúng ta sẽ không chạy bất kỳ hiệu ứng chuyển động liên kết cuộn nào.

if (motionOK) {
  // motion based animation code
}

Tại thời điểm viết bài này, trình duyệt không hỗ trợ @scroll-timeline. Đây là một bản nháp đặc tả chỉ có các phương thức triển khai thử nghiệm. Tuy nhiên, nó có một polyfill mà tôi sử dụng trong bản minh hoạ này.

ScrollTimeline

Mặc dù cả CSS và JavaScript đều có thể tạo dòng thời gian cuộn, nhưng tôi chọn JavaScript để có thể sử dụng các phép đo phần tử trực tiếp trong ảnh động.

const sectionScrollTimeline = new ScrollTimeline({
  scrollSource: tabsection,  // snap-tabs > section
  orientation: 'inline',     // scroll in the direction letters flow
  fill: 'both',              // bi-directional linking
});

Tôi muốn một thành phần theo dõi vị trí cuộn của một thành phần khác và bằng cách tạo ScrollTimeline, tôi xác định trình điều khiển của mối liên kết cuộn, đó là scrollSource. Thông thường, ảnh động trên web chạy theo một khung thời gian chung, nhưng với sectionScrollTimeline tuỳ chỉnh trong bộ nhớ, tôi có thể thay đổi tất cả.

tabindicator.animate({
    transform: ...,
    width: ...,
  }, {
    duration: 1000,
    fill: 'both',
    timeline: sectionScrollTimeline,
  }
);

Trước khi đi vào các khung hình chính của ảnh động, tôi nghĩ điều quan trọng là phải chỉ ra rằng phần tử theo dõi hoạt động cuộn tabindicator sẽ được tạo ảnh động dựa trên một dòng thời gian tuỳ chỉnh, tức là hoạt động cuộn của phần tử. Thao tác này hoàn tất việc liên kết, nhưng thiếu thành phần cuối cùng, đó là các điểm có trạng thái để tạo ảnh động, còn được gọi là khung hình chính.

Khung hình chính động

Có một cách sử dụng CSS khai báo thuần tuý rất mạnh mẽ để tạo ảnh động bằng @scroll-timeline, nhưng ảnh động mà tôi chọn thực hiện lại quá linh hoạt. Không có cách nào để chuyển đổi giữa chiều rộng auto và không có cách nào để tạo linh động một số khung hình chính dựa trên độ dài của các thành phần con.

Tuy nhiên, JavaScript biết cách lấy thông tin đó, vì vậy, chúng ta sẽ tự lặp lại các phần tử con và lấy các giá trị được tính tại thời gian chạy:

tabindicator.animate({
    transform: [...tabnavitems].map(({offsetLeft}) =>
      `translateX(${offsetLeft}px)`),
    width: [...tabnavitems].map(({offsetWidth}) =>
      `${offsetWidth}px`)
  }, {
    duration: 1000,
    fill: 'both',
    timeline: sectionScrollTimeline,
  }
);

Đối với mỗi tabnavitem, hãy phân tách vị trí offsetLeft và trả về một chuỗi sử dụng vị trí đó làm giá trị translateX. Thao tác này sẽ tạo 4 khung hình chính biến đổi cho ảnh động. Điều tương tự cũng xảy ra với chiều rộng, mỗi chiều rộng được hỏi chiều rộng động của nó là bao nhiêu, sau đó chiều rộng này được dùng làm giá trị khung hình chính.

Sau đây là ví dụ về đầu ra, dựa trên các lựa chọn ưu tiên về phông chữ và trình duyệt của tôi:

Khung hình chính TranslateX:

[...tabnavitems].map(({offsetLeft}) =>
  `translateX(${offsetLeft}px)`)

// results in 4 array items, which represent 4 keyframe states
// ["translateX(0px)", "translateX(121px)", "translateX(238px)", "translateX(464px)"]

Khung hình chính theo chiều rộng:

[...tabnavitems].map(({offsetWidth}) =>
  `${offsetWidth}px`)

// results in 4 array items, which represent 4 keyframe states
// ["121px", "117px", "226px", "67px"]

Để tóm tắt chiến lược này, chỉ báo thẻ hiện sẽ chuyển động trên 4 khung hình chính, tuỳ thuộc vào vị trí cuộn nhanh của trình cuộn phần. Các điểm bám tạo ra ranh giới rõ ràng giữa các khung hình chính và thực sự góp phần tạo nên cảm giác đồng bộ cho ảnh động.

thẻ đang hoạt động và thẻ không hoạt động xuất hiện với lớp phủ VisBug cho thấy điểm tương phản đạt yêu cầu cho cả hai

Người dùng điều khiển ảnh động bằng cách tương tác, thấy chiều rộng và vị trí của chỉ báo thay đổi từ phần này sang phần khác, theo dõi hoàn hảo bằng thao tác cuộn.

Có thể bạn không nhận thấy, nhưng tôi rất tự hào về sự chuyển đổi màu sắc khi mục điều hướng được làm nổi bật trở thành mục được chọn.

Màu xám nhạt chưa được chọn sẽ xuất hiện ở phía sau hơn nữa khi mục được đánh dấu có độ tương phản cao hơn. Việc chuyển đổi màu cho văn bản là điều thường thấy, chẳng hạn như khi di chuột và khi được chọn, nhưng việc chuyển đổi màu đó khi cuộn, đồng bộ hoá với chỉ báo gạch chân là một bước tiến mới.

Sau đây là cách tôi thực hiện:

tabnavitems.forEach(navitem => {
  navitem.animate({
      color: [...tabnavitems].map(item =>
        item === navitem
          ? `var(--text-active-color)`
          : `var(--text-color)`)
    }, {
      duration: 1000,
      fill: 'both',
      timeline: sectionScrollTimeline,
    }
  );
});

Mỗi đường liên kết điều hướng theo thẻ đều cần có hiệu ứng màu mới này, theo dõi cùng một dòng thời gian cuộn như chỉ báo gạch chân. Tôi sử dụng cùng một dòng thời gian như trước: vì vai trò của nó là phát ra một dấu đánh dấu khi cuộn, nên chúng ta có thể sử dụng dấu đánh dấu đó trong bất kỳ loại hoạt ảnh nào mà chúng ta muốn. Như trước đây, tôi tạo 4 khung hình chính trong vòng lặp và trả về màu sắc.

[...tabnavitems].map(item =>
  item === navitem
    ? `var(--text-active-color)`
    : `var(--text-color)`)

// results in 4 array items, which represent 4 keyframe states
// [
  "var(--text-active-color)",
  "var(--text-color)",
  "var(--text-color)",
  "var(--text-color)",
]

Khung hình chính có màu var(--text-active-color) làm nổi bật đường liên kết, nếu không thì đó là màu văn bản tiêu chuẩn. Vòng lặp lồng nhau ở đó giúp việc này tương đối đơn giản, vì vòng lặp bên ngoài là từng mục điều hướng và vòng lặp bên trong là từng khung hình khoá cá nhân của mục điều hướng. Tôi kiểm tra xem phần tử vòng lặp bên ngoài có giống với phần tử vòng lặp bên trong hay không và sử dụng phần tử đó để biết thời điểm phần tử được chọn.

Tôi rất vui khi viết bài này. Rất nhiều.

Nhiều điểm cải tiến hơn nữa về JavaScript

Bạn nên nhớ rằng cốt lõi của những gì tôi đang cho bạn thấy ở đây hoạt động mà không cần JavaScript. Giờ chúng ta đã nắm rõ các công cụ này, hãy xem cách cải thiện khi có sẵn JS.

Đường liên kết sâu là một thuật ngữ dành cho thiết bị di động, nhưng tôi nghĩ ý định của đường liên kết sâu được đáp ứng ở đây bằng các thẻ, theo đó bạn có thể chia sẻ trực tiếp một URL đến nội dung của thẻ. Trình duyệt sẽ chuyển đến mã nhận dạng được so khớp trong hàm băm URL trên trang. Tôi nhận thấy trình xử lý onload này tạo ra hiệu ứng trên nhiều nền tảng.

window.onload = () => {
  if (location.hash) {
    tabsection.scrollLeft = document
      .querySelector(location.hash)
      .offsetLeft;
  }
}

Đồng bộ hoá khi kết thúc cử chỉ cuộn

Người dùng không phải lúc nào cũng nhấp hoặc sử dụng bàn phím, đôi khi họ chỉ cuộn tự do, vì họ có thể làm như vậy. Khi trình cuộn phần dừng cuộn, bất cứ nơi nào trình cuộn dừng lại đều phải khớp với thanh điều hướng trên cùng.

Sau đây là cách tôi đợi thao tác cuộn kết thúc: js tabsection.addEventListener('scroll', () => { clearTimeout(tabsection.scrollEndTimer); tabsection.scrollEndTimer = setTimeout(determineActiveTabSection, 100); });

Bất cứ khi nào các phần được cuộn, hãy xoá thời gian chờ của phần nếu có và bắt đầu một thời gian chờ mới. Khi các phần ngừng cuộn, đừng xoá thời gian chờ và kích hoạt 100 mili giây sau khi dừng. Khi sự kiện này kích hoạt, hãy gọi hàm tìm cách xác định vị trí mà người dùng đã dừng.

const determineActiveTabSection = () => {
  const i = tabsection.scrollLeft / tabsection.clientWidth;
  const matchingNavItem = tabnavitems[i];

  matchingNavItem && setActiveTab(matchingNavItem);
};

Giả sử thao tác cuộn được thực hiện nhanh, việc chia vị trí cuộn hiện tại cho chiều rộng của vùng cuộn sẽ cho ra một số nguyên chứ không phải số thập phân. Sau đó, tôi cố gắng lấy một navitem từ bộ nhớ đệm của chúng tôi thông qua chỉ mục được tính này và nếu tìm thấy thứ gì đó, tôi sẽ gửi kết quả trùng khớp để đặt thành hoạt động.

const setActiveTab = tabbtn => {
  tabnav
    .querySelector(':scope a[active]')
    .removeAttribute('active');

  tabbtn.setAttribute('active', '');
  tabbtn.scrollIntoView();
};

Việc đặt thẻ đang hoạt động bắt đầu bằng cách xoá mọi thẻ hiện đang hoạt động, sau đó cấp cho mục điều hướng đến trạng thái đang hoạt động. Lệnh gọi đến scrollIntoView() có một lượt tương tác thú vị với CSS mà bạn nên lưu ý.

.scroll-snap-x {
  overflow: auto hidden;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;

  @media (prefers-reduced-motion: no-preference) {
    scroll-behavior: smooth;
  }
}

Trong CSS tiện ích chụp cuộn ngang, chúng ta đã lồng một truy vấn đa phương tiện áp dụng chế độ cuộn smooth nếu người dùng không bị ảnh hưởng bởi chuyển động. JavaScript có thể tự do thực hiện các lệnh gọi để cuộn các phần tử vào chế độ xem và CSS có thể quản lý trải nghiệm người dùng một cách khai báo. Đôi khi, chúng tạo ra những cặp đôi nhỏ bé nhưng vô cùng thú vị.

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í trong khung yêu thích của họ? 🙂

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