Chuyển đổi cùng tài liệu

Vấn đề

Các trang web thường cung cấp nhiều chế độ xem cho một đối tượng, chẳng hạn như danh sách sản phẩm, sau đó là trang thông tin chi tiết cho từng sản phẩm. Việc di chuyển giữa hai chế độ xem thường có cảm giác không liền mạch. Khi người dùng nhấp vào hình thu nhỏ của sản phẩm để xem thông tin chi tiết, hình thu nhỏ sẽ biến mất và một hình ảnh mới, lớn hơn sẽ xuất hiện ngay lập tức ở một vị trí khác trên màn hình. Việc thiếu tính liên tục này khiến người dùng khó theo dõi mối quan hệ giữa các phần tử hơn.

Giải pháp

View Transitions API cho phép bạn chỉ định các cặp phần tử tồn tại ở nhiều trạng thái trước và sau khi chuyển đổi. Khi kích hoạt hiệu ứng chuyển đổi bằng document.startViewTransition() trong tính năng Điều hướng một trang (SPA), trình duyệt sẽ xác định các phần tử dùng chung này bằng view-transition-name riêng biệt được chia sẻ. Sau đó, API này sẽ tự động tính toán sự khác biệt về vị trí, kích thước và kiểu dáng, đồng thời tạo ảnh động mượt mà từ trạng thái cũ sang trạng thái mới. Hiệu ứng chuyển đổi này xảy ra ở lớp trên cùng, thậm chí phía trên các phần tử có giá trị z-index cao.

Hướng dẫn triển khai

Bước 1: Gói các thay đổi về trạng thái trong startViewTransition

Đối với Ứng dụng một trang (SPA) hoặc các thay đổi trạng thái đơn giản, hãy gói logic cập nhật DOM trong document.startViewTransition. Trình duyệt chụp nhanh trạng thái hiện tại, chạy bản cập nhật rồi chụp trạng thái mới.

function navigate(view) {
  // MANDATORY: Wrap the update in startViewTransition
  document.startViewTransition(() => updateDOM(view));
}

Bước 2: Chỉ định tên chuyển đổi chung

Sử dụng thuộc tính CSS view-transition-name để cho trình duyệt biết phần tử nào cần được biến đổi. Tên có thể là bất kỳ tên nào (ngoại trừ none). BẮT BUỘC: không được có nhiều hơn 1 phần tử trước và sau một view-transition-name nhất định. Nếu có từ 2 phần tử trở lên có một view-transition-name nhất định, thì DOM sẽ được cập nhật ngay lập tức sang trạng thái mới mà không cần chuyển đổi.

Bạn có thể sử dụng nhiều view-transition-name để biến đổi nhiều cặp phần tử. Ví dụ: bạn có thể muốn chuyển đổi cả hình ảnh và tiêu đề sản phẩm bằng các hiệu ứng chuyển đổi riêng biệt.

Vì có nhiều mục trong chế độ xem danh sách, nên bạn không thể đặt cùng một view-transition-name cho tất cả các mục đó. Bạn có thể giải quyết vấn đề này theo hai cách trong SPA.

  1. Trang thông tin chi tiết động: Chỉ định cho mỗi mục trên trang danh sách một view-transition-name riêng biệt, sau đó áp dụng tên đó một cách linh động cho phần tử phù hợp trên trang thông tin chi tiết khi mục danh sách được chọn, như minh hoạ tại đây.

    /* In the list view, give each */
    #product-1 { view-transition-name: p1 }
    #product-2 { view-transition-name: p2 }
    #product-3 { view-transition-name: p3 }
    
    function updateDOM(clickedTransitionName){
      const hero = document.getElementById("hero");
      hero.style.viewTransitionName = clickedTransitionName;
    }
    
  2. Mục danh sách động: Chỉ định cho phần tử trên trang thông tin chi tiết một view-transition-name và áp dụng tên đó cho mục trên trang danh sách khi mục đó được chọn. Xoá view-transition-name khỏi mục trên trang danh sách khi quay lại trang danh sách.

Phần tử #hero trên trang thông tin chi tiết và phần tử .thumbnail đã chọn trên trang danh sách có chung một view-transition-name.

#hero{
  view-transition-name: hero;
}
.thumbnail.selected {
  view-transition-name: hero;
}

Khi người dùng nhấp vào một hình thu nhỏ, chúng ta cần chuẩn bị chế độ xem danh sách bằng cách chỉ định view-transition-name bằng bộ chọn lớp .selected và thực hiện mọi thay đổi đối với DOM trước khi bắt đầu chuyển đổi.

Sau đó, bạn có thể gọi document.startViewTransition() và áp dụng các thay đổi để chuyển đổi trang từ chế độ xem chi tiết sang chế độ xem danh sách.

Sau khi quay lại chế độ xem danh sách, bạn phải dọn dẹp các lớp chuyển đổi khung hiển thị để ngăn lỗi xảy ra trong lần điều hướng tiếp theo. Bạn có thể thực hiện việc dọn dẹp này sau khi lời hứa finished của quá trình chuyển đổi được giải quyết.

// Function called when a thumbnail is clicked
function goFromListToDetail(e){
  e.currentTarget.classList.add("selected");
  const hero = document.getElementById("hero");
  const bgColor = getComputedStyle(e.currentTarget).backgroundColor;
  hero.style.background = bgColor;

  // Trigger the transition, checking for support
  if (!document.startViewTransition) {
    document.body.classList.add("detail");
    // MANDATORY Accessibility Routing: Route focus to the newly revealed heading to announce context and preserve logical tab flow
    document.getElementById("detail-heading")?.focus();
    return; // MANDATORY: End function execution if view transitions are not supported.  
  }
  const transition = document.startViewTransition(() => {
    document.body.classList.add("detail");
  });
  // MANDATORY Accessibility Routing: Route focus after the view transition resolves
  transition.finished.finally(() => {
    document.getElementById("detail-heading")?.focus();
  });
}

// Function called when navigating from detail back to list view
function goFromDetailToList() {
  if (!document.startViewTransition) {
    document.body.classList.remove("detail");
    document.getElementById("list-heading")?.focus();
    return;
  }
  const transition = document.startViewTransition(() => {
    document.body.classList.remove("detail");
  });
  // Clean up the list view and route focus
  transition.finished.finally(() => {
    // Route focus back to list view
    document.getElementById("list-heading")?.focus();
    // Remove selected classList to remove view-transition-names
    document.querySelectorAll(".selected").forEach(
      (element) => {
        element.classList.remove("selected");
      },
    );
  });
}

Phương thức bạn chọn sẽ phụ thuộc vào trường hợp sử dụng. Mục danh sách động yêu cầu ít CSS lặp lại hơn, nhưng cần dọn dẹp JavaScript theo cách thủ công nhiều hơn.

Bước 3: Khắc phục lỗi "kéo giãn" tỷ lệ khung hình

Theo mặc định, trình duyệt sẽ làm mờ dần ảnh chụp nhanh cũ và mới trong một nhóm kéo dài để vừa với cả hai. Nếu bạn đang chuyển đổi văn bản, hãy đặt chiều rộng của phần tử văn bản thành fit-content trên cả chế độ xem cũ và mới để tỷ lệ khung hình của phần tử được chuyển đổi luôn ổn định.

#list-page .title {
  width: fit-content;
}

#detail-page #title {
  width: fit-content;
}

Nếu đang chuyển đổi các phần tử thay đổi tỷ lệ khung hình, bạn có thể cần đặt chiều cao của các phần tử giả cũ và mới thành 100% của phần tử giả ::view-transition-pair().

::view-transition-old(hero),
::view-transition-new(hero){
  height: 100%;
}

Các phần tử giả là ảnh chụp nhanh của các phần tử đang hoạt động, vì vậy, bạn cũng có thể sử dụng các khai báo object-fit và object-position để kiểm soát hiệu ứng chuyển đổi hiệu quả hơn.

Các phương pháp hay nhất

  • KHÔNG chỉ định quá nhiều hiệu ứng chuyển đổi. Chỉ sử dụng các phần tử dùng chung cho nội dung chính mà người dùng đang tích cực theo dõi hoạt động (ví dụ: hình ảnh chính, tiêu đề).
  • NÊN xoá các giá trị view-transition-name tạm thời sau khi quá trình chuyển đổi kết thúc để tránh tác dụng phụ đối với các quá trình chuyển đổi trong tương lai.
  • KHÔNG chuyển đổi các phần tử có ảnh động đang hoạt động. Hiệu ứng chuyển đổi khung hiển thị hoạt động trên ảnh chụp nhanh, vì vậy, mọi ảnh động sẽ có vẻ như bị tạm dừng trong quá trình chuyển đổi khung hiển thị.
  • NÊN tôn trọng lựa chọn ưu tiên của người dùng về việc giảm chuyển động bằng cách sử dụng truy vấn phương tiện prefers-reduced-motion.
  • Định tuyến hỗ trợ tiếp cận BẮT BUỘC: Hiệu ứng chuyển đổi khung hiển thị biến đổi bố cục trang một cách linh động nhưng không quản lý tiêu điểm có lập trình. Nếu tiêu điểm vẫn nằm trên một phần tử bị ẩn hoặc bị xoá trong quá trình chuyển đổi, thì tiêu điểm sẽ bị bỏ qua, khiến người dùng bàn phím và công nghệ hỗ trợ không có ngữ cảnh. Chuyển tiêu điểm theo phương thức lập trình sang tiêu đề trang hoặc vùng chứa khung hiển thị đã cập nhật (bằng cách sử dụng tabindex="-1") ngay sau khi DOM cập nhật hoặc khi lời hứa finished của quá trình chuyển đổi khung hiển thị được thực hiện.
@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }
}

Chiến lược dự phòng

View Transitions API được thiết kế để cải tiến tăng dần. Các trình duyệt không hỗ trợ API này sẽ chỉ thực thi bản cập nhật DOM ngay lập tức mà không có ảnh động.

function navigate(){
  if (!document.startViewTransition) {
    // Fallback: Just update the DOM
    updateDOM();
  } else {
    document.startViewTransition(() => updateDOM());
  }
}