การเปลี่ยนมุมมองเอกสารเดียวกันสำหรับแอปพลิเคชันหน้าเว็บเดียว

เผยแพร่: 17 ส.ค. 2021, อัปเดตล่าสุด: 25 ก.ย. 2024

เมื่อทรานซิชันของมุมมองทำงานในเอกสารเดียว จะเรียกว่าทรานซิชันของมุมมองในเอกสารเดียวกัน กรณีนี้มักเกิดขึ้นในแอปพลิเคชันหน้าเว็บเดียว (SPA) ที่ใช้ JavaScript เพื่ออัปเดต DOM Chrome รองรับการเปลี่ยนมุมมองเอกสารเดียวกันตั้งแต่ Chrome เวอร์ชัน 111

หากต้องการทริกเกอร์การเปลี่ยนมุมมองเอกสารเดียวกัน ให้เรียกใช้ document.startViewTransition ดังนี้

function handleClick(e) {
  // Fallback for browsers that don't support this API:
  if (!document.startViewTransition) {
    updateTheDOMSomehow();
    return;
  }

  // With a View Transition:
  document.startViewTransition(() => updateTheDOMSomehow());
}

เมื่อเรียกใช้ เบราว์เซอร์จะจับภาพหน้าจอขององค์ประกอบทั้งหมดที่มีการประกาศพร็อพเพอร์ตี้ view-transition-name CSS โดยอัตโนมัติ

จากนั้นจะเรียกใช้การเรียกกลับที่ส่งผ่านซึ่งอัปเดต DOM หลังจากนั้นจะจับภาพสถานะใหม่

จากนั้นระบบจะจัดเรียงภาพเหล่านี้เป็นลําดับชั้นขององค์ประกอบจำลองและสร้างภาพเคลื่อนไหวโดยใช้ความสามารถของภาพเคลื่อนไหว CSS ภาพนิ่ง 2 คู่จากสถานะเก่าและใหม่จะเปลี่ยนจากตำแหน่งและขนาดเดิมไปยังตำแหน่งใหม่อย่างราบรื่น ขณะที่เนื้อหาของภาพนิ่งจะค่อยๆ เปลี่ยน คุณใช้ CSS เพื่อปรับแต่งภาพเคลื่อนไหวได้หากต้องการ


ทรานซิชันเริ่มต้น: เฟดเข้า

การเปลี่ยนฉากเริ่มต้นคือการเปลี่ยนภาพแบบ Cross-fade จึงเหมาะที่จะใช้เป็นข้อมูลเบื้องต้นเกี่ยวกับ API

function spaNavigate(data) {
  // Fallback for browsers that don't support this API:
  if (!document.startViewTransition) {
    updateTheDOMSomehow(data);
    return;
  }

  // With a transition:
  document.startViewTransition(() => updateTheDOMSomehow(data));
}

โดยที่ updateTheDOMSomehow จะเปลี่ยน DOM เป็นสถานะใหม่ คุณดำเนินการดังกล่าวได้ตามต้องการ เช่น คุณสามารถเพิ่มหรือนำองค์ประกอบออก เปลี่ยนชื่อคลาส หรือเปลี่ยนสไตล์

หน้าเว็บจะเปลี่ยนจากหน้าหนึ่งไปยังอีกหน้าหนึ่งโดยอัตโนมัติ

การเฟดเสียงแบบข้ามเริ่มต้น การสาธิตแบบจำกัด แหล่งที่มา

โอเค เสียงที่ตัดไปมานั้นไม่ค่อยน่าประทับใจเท่าไหร่ แต่คุณปรับแต่งทรานซิชันได้ แต่ก่อนอื่นคุณต้องเข้าใจวิธีการทํางานของทรานซิชันแบบครอสเฟดพื้นฐานนี้


วิธีการทํางานของทรานซิชันเหล่านี้

มาอัปเดตโค้ดตัวอย่างก่อนหน้ากัน

document.startViewTransition(() => updateTheDOMSomehow(data));

เมื่อเรียก .startViewTransition() แล้ว API จะบันทึกสถานะปัจจุบันของหน้าเว็บ ซึ่งรวมถึงการจับภาพหน้าจอ

เมื่อเสร็จแล้ว ระบบจะเรียกใช้การเรียกกลับที่ส่งไปยัง .startViewTransition() ซึ่ง DOM จะเปลี่ยนแปลงในส่วนนี้ จากนั้น API จะบันทึกสถานะใหม่ของหน้าเว็บ

เมื่อบันทึกสถานะใหม่แล้ว API จะสร้างต้นไม้องค์ประกอบจำลองดังนี้

::view-transition
└─ ::view-transition-group(root)
   └─ ::view-transition-image-pair(root)
      ├─ ::view-transition-old(root)
      └─ ::view-transition-new(root)

::view-transition จะวางซ้อนอยู่เหนือสิ่งอื่นๆ ในหน้า ซึ่งจะเป็นประโยชน์หากคุณต้องการตั้งค่าสีพื้นหลังของทรานซิชัน

::view-transition-old(root) คือภาพหน้าจอของมุมมองเก่า และ ::view-transition-new(root) คือการแสดงมุมมองใหม่แบบเรียลไทม์ ทั้ง 2 รายการจะแสดงผลเป็น "เนื้อหาที่แทนที่" ของ CSS (เช่น <img>)

มุมมองเก่าจะเคลื่อนไหวจาก opacity: 1 ไปยัง opacity: 0 ส่วนมุมมองใหม่จะเคลื่อนไหวจาก opacity: 0 ไปยัง opacity: 1 ซึ่งจะทำให้เกิดภาพซ้อนทับ

ภาพเคลื่อนไหวทั้งหมดจะทำงานโดยใช้ภาพเคลื่อนไหว CSS จึงปรับแต่งได้ด้วย CSS

ปรับแต่งทรานซิชัน

องค์ประกอบจำลองการเปลี่ยนของมุมมองทั้งหมดสามารถกําหนดเป้าหมายด้วย CSS ได้ และเนื่องจากภาพเคลื่อนไหวได้รับการกําหนดโดยใช้ CSS คุณจึงแก้ไขภาพเคลื่อนไหวได้โดยใช้คุณสมบัติภาพเคลื่อนไหว CSS ที่มีอยู่ เช่น

::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 5s;
}

การเปลี่ยนแปลงเพียงอย่างเดียวนี้ทำให้การเลือนออกช้ามาก

Cross-fade แบบยาว การสาธิตแบบจำกัด แหล่งที่มา

โอเค ตัวเลขนี้ยังไม่น่าประทับใจ แต่จะใช้การเปลี่ยนแกนที่ใช้ร่วมกันของ Material Design แทน

@keyframes fade-in {
  from { opacity: 0; }
}

@keyframes fade-out {
  to { opacity: 0; }
}

@keyframes slide-from-right {
  from { transform: translateX(30px); }
}

@keyframes slide-to-left {
  to { transform: translateX(-30px); }
}

::view-transition-old(root) {
  animation: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
}

::view-transition-new(root) {
  animation: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
}

ผลลัพธ์ที่ได้มีดังนี้

การเปลี่ยนแกนที่ใช้ร่วมกัน การสาธิตแบบจำกัด แหล่งที่มา

เปลี่ยนองค์ประกอบหลายรายการ

ในการแสดงตัวอย่างก่อนหน้านี้ หน้าเว็บทั้งหน้าจะมีส่วนร่วมในการเปลี่ยนแปลงแกนที่ใช้ร่วมกัน การดำเนินการนี้ใช้ได้กับส่วนต่างๆ ของหน้าส่วนใหญ่ แต่ดูเหมือนจะไม่เหมาะกับส่วนหัว เนื่องจากส่วนหัวจะเลื่อนออกแล้วเลื่อนกลับเข้ามาอีกครั้ง

หากต้องการหลีกเลี่ยงปัญหานี้ ให้ดึงส่วนหัวออกจากส่วนที่เหลือของหน้าเพื่อให้เคลื่อนไหวแยกกันได้ ซึ่งทำได้โดยการกําหนด view-transition-name ให้กับองค์ประกอบ

.main-header {
  view-transition-name: main-header;
}

ค่าของ view-transition-name จะเป็นอะไรก็ได้ตามต้องการ (ยกเว้น none ซึ่งหมายความว่าไม่มีชื่อทรานซิชัน) ซึ่งจะใช้เพื่อระบุองค์ประกอบโดยไม่ซ้ำกันตลอดการเปลี่ยน

และผลลัพธ์ที่ได้คือ

การเปลี่ยนแกนที่ใช้ร่วมกันซึ่งมีส่วนหัวแบบคงที่ การสาธิตแบบจำกัด แหล่งที่มา

ตอนนี้ส่วนหัวจะยังคงอยู่ในตำแหน่งเดิมและตัดต่อแบบ Cross-fade

การประกาศ CSS ดังกล่าวทําให้ต้นไม้องค์ประกอบสมมติมีการเปลี่ยนแปลง

::view-transition
├─ ::view-transition-group(root)
│  └─ ::view-transition-image-pair(root)
│     ├─ ::view-transition-old(root)
│     └─ ::view-transition-new(root)
└─ ::view-transition-group(main-header)
   └─ ::view-transition-image-pair(main-header)
      ├─ ::view-transition-old(main-header)
      └─ ::view-transition-new(main-header)

ตอนนี้มีกลุ่มการเปลี่ยน 2 กลุ่ม 1 รายการสำหรับส่วนหัว และอีก 1 รายการสำหรับส่วนที่เหลือ รายการเหล่านี้สามารถกําหนดเป้าหมายด้วย CSS แยกต่างหากและกำหนดการเปลี่ยนภาพที่แตกต่างกันได้ อย่างไรก็ตาม ในกรณีนี้ main-header ยังคงใช้ทรานซิชันเริ่มต้นซึ่งเป็นการเฟดเข้าด้วยกัน

การเปลี่ยนเริ่มต้นไม่ใช่แค่การเฟดเข้าออกเท่านั้น ::view-transition-group ยังเปลี่ยนรูปแบบด้วย

  • ตำแหน่งและการเปลี่ยนรูปแบบ (ใช้ transform)
  • ความกว้าง
  • ส่วนสูง

ประเด็นนี้ไม่ได้สำคัญจนกระทั่งตอนนี้ เนื่องจากส่วนหัวมีขนาดและตำแหน่งเหมือนกันทั้ง 2 ด้านของการเปลี่ยนแปลง DOM แต่คุณยังดึงข้อความในส่วนหัวได้ด้วย โดยทำดังนี้

.main-header-text {
  view-transition-name: main-header-text;
  width: fit-content;
}

fit-content ใช้เพื่อให้องค์ประกอบมีขนาดเท่ากับข้อความแทนที่จะยืดให้เต็มความกว้างที่เหลือ หากไม่มีค่านี้ ลูกศรย้อนกลับจะลดขนาดขององค์ประกอบข้อความส่วนหัวแทนที่จะมีขนาดเท่ากันในทั้ง 2 หน้า

ตอนนี้เรามี 3 ส่วนให้เล่นด้วย ได้แก่

::view-transition
├─ ::view-transition-group(root)
│  └─ …
├─ ::view-transition-group(main-header)
│  └─ …
└─ ::view-transition-group(main-header-text)
   └─ …

แต่อีกครั้ง เพียงใช้ค่าเริ่มต้น

ข้อความส่วนหัวแบบเลื่อน การสาธิตแบบจำกัด แหล่งที่มา

ตอนนี้ข้อความส่วนหัวจะเลื่อนไปด้านข้างเล็กน้อยเพื่อให้มีพื้นที่สำหรับปุ่มย้อนกลับ


สร้างภาพเคลื่อนไหวขององค์ประกอบจำลองหลายรายการในลักษณะเดียวกันด้วย view-transition-class

การรองรับเบราว์เซอร์

  • Chrome: 125
  • Edge: 125
  • Firefox: ไม่รองรับ
  • Safari: 18.2

สมมติว่าคุณมีทรานซิชันของมุมมองที่มีการ์ดจํานวนมาก แต่ก็มีชื่อในหน้าด้วย หากต้องการทำให้การ์ดทั้งหมดเคลื่อนไหวยกเว้นชื่อ คุณต้องเขียนตัวเลือกที่กำหนดเป้าหมายไปยังการ์ดแต่ละใบ

h1 {
    view-transition-name: title;
}
::view-transition-group(title) {
    animation-timing-function: ease-in-out;
}

#card1 { view-transition-name: card1; }
#card2 { view-transition-name: card2; }
#card3 { view-transition-name: card3; }
#card4 { view-transition-name: card4; }

#card20 { view-transition-name: card20; }

::view-transition-group(card1),
::view-transition-group(card2),
::view-transition-group(card3),
::view-transition-group(card4),

::view-transition-group(card20) {
    animation-timing-function: var(--bounce);
}

มีองค์ประกอบ 20 รายการไหม คุณจะต้องเขียนตัวเลือก 20 รายการ การเพิ่มองค์ประกอบใหม่ จากนั้นคุณยังต้องขยายตัวเลือกที่ใช้สไตล์ภาพเคลื่อนไหวด้วย ปรับขนาดได้ไม่ดีนัก

คุณสามารถใช้ view-transition-class ในองค์ประกอบจำลองการเปลี่ยนมุมมองเพื่อใช้กฎสไตล์เดียวกัน

#card1 { view-transition-name: card1; }
#card2 { view-transition-name: card2; }
#card3 { view-transition-name: card3; }
#card4 { view-transition-name: card4; }
#card5 { view-transition-name: card5; }

#card20 { view-transition-name: card20; }

#cards-wrapper > div {
  view-transition-class: card;
}
html::view-transition-group(.card) {
  animation-timing-function: var(--bounce);
}

ตัวอย่างการ์ดต่อไปนี้ใช้ประโยชน์จากข้อมูลโค้ด CSS ก่อนหน้า การ์ดทั้งหมด รวมถึงการ์ดที่เพิ่มใหม่จะใช้ช่วงเวลาเดียวกันโดยใช้ตัวเลือกเดียว html::view-transition-group(.card)

ไฟล์บันทึกเสียงการสาธิตการ์ด เมื่อใช้ view-transition-class ระบบจะใช้ animation-timing-function เดียวกันกับการ์ดทั้งหมด ยกเว้นการ์ดที่เพิ่มหรือนําออก

แก้ไขข้อบกพร่องของทรานซิชัน

เนื่องจากทรานซิชันของมุมมองสร้างขึ้นจากภาพเคลื่อนไหว CSS แผงภาพเคลื่อนไหวในเครื่องมือสำหรับนักพัฒนาเว็บใน Chrome จึงเหมาะอย่างยิ่งสำหรับการแก้ไขข้อบกพร่องของทรานซิชัน

เมื่อใช้แผงภาพเคลื่อนไหว คุณจะหยุดภาพเคลื่อนไหวถัดไปชั่วคราว แล้วกรอภาพเคลื่อนไหวไปมาก็ได้ ในระหว่างนี้ คุณจะเห็นองค์ประกอบจำลองของทรานซิชันในแผงองค์ประกอบ

การแก้ไขข้อบกพร่องการเปลี่ยนมุมมองด้วยเครื่องมือสำหรับนักพัฒนาเว็บใน Chrome

องค์ประกอบการเปลี่ยนไม่จำเป็นต้องเป็นองค์ประกอบ DOM เดียวกัน

จนถึงตอนนี้เราใช้ view-transition-name เพื่อสร้างองค์ประกอบทรานซิชันแยกต่างหากสำหรับส่วนหัวและข้อความในส่วนหัว องค์ประกอบเหล่านี้เป็นองค์ประกอบเดียวกันทั้งก่อนและหลังการเปลี่ยนแปลง DOM แต่คุณสร้างทรานซิชันที่ไม่ใช่แบบนั้นก็ได้

เช่น คุณสามารถกำหนด view-transition-name ให้กับวิดีโอที่ฝังหลักได้ดังนี้

.full-embed {
  view-transition-name: full-embed;
}

จากนั้นเมื่อมีการคลิกภาพปก view-transition-name เดียวกันนี้ก็สามารถแสดงได้ในช่วงการเปลี่ยนภาพ

thumbnail.onclick = async () => {
  thumbnail.style.viewTransitionName = 'full-embed';

  document.startViewTransition(() => {
    thumbnail.style.viewTransitionName = '';
    updateTheDOMSomehow();
  });
};

และผลลัพธ์ที่ได้คือ

องค์ประกอบหนึ่งเปลี่ยนเป็นองค์ประกอบอื่น การสาธิตแบบจำกัด แหล่งที่มา

ตอนนี้ภาพปกจะเปลี่ยนเป็นรูปภาพหลัก แม้ว่าจะเป็นองค์ประกอบที่แตกต่างกันในแง่แนวคิด (และตามตัวอักษร) แต่ Transition API จะถือว่าองค์ประกอบเหล่านี้เหมือนกันเนื่องจากมี view-transition-name เดียวกัน

โค้ดจริงสำหรับการเปลี่ยนนี้มีความซับซ้อนกว่าตัวอย่างก่อนหน้านี้เล็กน้อย เนื่องจากต้องจัดการการเปลี่ยนกลับไปที่หน้าภาพขนาดย่อด้วย ดูแหล่งที่มาสําหรับการใช้งานแบบเต็ม


ทรานซิชันการเข้าและออกที่กำหนดเอง

ดูตัวอย่างนี้

การเข้าสู่และออกจากแถบด้านข้าง การสาธิตแบบจำกัด แหล่งที่มา

แถบด้านข้างเป็นส่วนหนึ่งของการเปลี่ยนแปลงนี้

.sidebar {
  view-transition-name: sidebar;
}

แต่ต่างจากส่วนหัวในตัวอย่างก่อนหน้านี้ตรงที่แถบด้านข้างจะไม่ปรากฏในบางหน้า หากทั้ง 2 สถานะมีแถบด้านข้าง องค์ประกอบจำลองการเปลี่ยนรูปแบบจะมีลักษณะดังนี้

::view-transition
├─ …other transition groups…
└─ ::view-transition-group(sidebar)
   └─ ::view-transition-image-pair(sidebar)
      ├─ ::view-transition-old(sidebar)
      └─ ::view-transition-new(sidebar)

อย่างไรก็ตาม หากแถบด้านข้างอยู่ในหน้าใหม่เท่านั้น เอลิเมนต์จำลอง ::view-transition-old(sidebar) จะไม่ปรากฏ เนื่องจากไม่มีรูปภาพ "เก่า" สำหรับแถบด้านข้าง คู่รูปภาพจึงมีเพียง ::view-transition-new(sidebar) เท่านั้น ในทํานองเดียวกัน หากแถบด้านข้างอยู่ในหน้าเก่าเท่านั้น คู่รูปภาพจะมีเพียง ::view-transition-old(sidebar)

ในการแสดงตัวอย่างก่อนหน้านี้ แถบด้านข้างจะเปลี่ยนสถานะแตกต่างกันไป ขึ้นอยู่กับว่ากำลังปรากฏ กำลังจะหายไป หรือปรากฏอยู่ในทั้ง 2 สถานะ โดยไอคอนจะปรากฏขึ้นโดยเลื่อนจากด้านขวาและค่อย ๆ ปรากฏขึ้น แล้วจะหายไปโดยเลื่อนไปทางขวาและค่อย ๆ หายไป และไอคอนจะยังคงอยู่ในตำแหน่งเดิมเมื่อแสดงในทั้ง 2 สถานะ

หากต้องการสร้างทรานซิชันของช่วงเข้าและช่วงออกที่เฉพาะเจาะจง คุณสามารถใช้:only-childคลาสจำลองเพื่อกำหนดเป้าหมายองค์ประกอบจำลองเก่าหรือใหม่เมื่อเป็นองค์ประกอบย่อยเพียงรายการเดียวในคู่รูปภาพ ดังนี้

/* Entry transition */
::view-transition-new(sidebar):only-child {
  animation: 300ms cubic-bezier(0, 0, 0.2, 1) both fade-in,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
}

/* Exit transition */
::view-transition-old(sidebar):only-child {
  animation: 150ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-right;
}

ในกรณีนี้ ไม่มีการเปลี่ยนภาพเฉพาะเมื่อแถบด้านข้างแสดงอยู่ในทั้ง 2 สถานะ เนื่องจากค่าเริ่มต้นนั้นสมบูรณ์แบบอยู่แล้ว

การอัปเดต DOM แบบไม่พร้อมกันและรอเนื้อหา

ฟังก์ชันการเรียกกลับที่ส่งไปยัง .startViewTransition() สามารถแสดงผลพรอมต์ ซึ่งจะทําให้ DOM อัปเดตแบบแอซิงค์และรอให้เนื้อหาที่สําคัญพร้อมใช้งาน

document.startViewTransition(async () => {
  await something;
  await updateTheDOMSomehow();
  await somethingElse;
});

การเปลี่ยนจะไม่เริ่มจนกว่าจะมีการปฏิบัติตามสัญญา ในระหว่างนี้ หน้าเว็บจะหยุดทำงานชั่วคราว คุณจึงควรลดการหน่วงเวลาให้เหลือน้อยที่สุด กล่าวโดยละเอียดคือ การดึงข้อมูลเครือข่ายควรทําก่อนเรียกใช้ .startViewTransition() ขณะที่หน้าเว็บยังโต้ตอบได้อย่างเต็มที่ แทนที่จะดึงข้อมูลเป็นส่วนหนึ่งของการเรียกกลับ .startViewTransition()

หากตัดสินใจที่จะรอให้รูปภาพหรือแบบอักษรพร้อมใช้งาน ให้ใช้การหมดเวลาแบบเร่งด่วน ดังนี้

const wait = ms => new Promise(r => setTimeout(r, ms));

document.startViewTransition(async () => {
  updateTheDOMSomehow();

  // Pause for up to 100ms for fonts to be ready:
  await Promise.race([document.fonts.ready, wait(100)]);
});

อย่างไรก็ตาม ในบางกรณี คุณควรหลีกเลี่ยงการเลื่อนเวลาออกทั้งหมดและใช้เนื้อหาที่มีอยู่แล้ว


ใช้เนื้อหาที่มีอยู่ให้เกิดประโยชน์สูงสุด

ในกรณีที่ภาพขนาดย่อเปลี่ยนเป็นภาพขนาดใหญ่ขึ้น

ภาพปกเปลี่ยนเป็นภาพขนาดใหญ่ขึ้น ลองใช้เว็บไซต์เดโม

การเปลี่ยนเริ่มต้นคือการเปลี่ยนภาพแบบครอสเฟด ซึ่งหมายความว่าภาพขนาดย่ออาจเปลี่ยนภาพแบบครอสเฟดกับรูปภาพขนาดเต็มที่ยังโหลดไม่เสร็จ

วิธีจัดการปัญหานี้อย่างหนึ่งคือรอให้รูปภาพโหลดจนเต็มก่อนเริ่มการเปลี่ยน โดยควรดำเนินการนี้ก่อนเรียกใช้ .startViewTransition() เพื่อให้หน้าเว็บยังคงมีการโต้ตอบ และสามารถแสดงภาพสปินเนอร์เพื่อแจ้งให้ผู้ใช้ทราบว่าระบบกําลังโหลดข้อมูล แต่ในกรณีนี้ มีวิธีที่ดีกว่าดังนี้

::view-transition-old(full-embed),
::view-transition-new(full-embed) {
  /* Prevent the default animation,
  so both views remain opacity:1 throughout the transition */
  animation: none;
  /* Use normal blending,
  so the new view sits on top and obscures the old view */
  mix-blend-mode: normal;
}

ตอนนี้ภาพขนาดย่อจะไม่หายไป แต่จะแสดงอยู่ใต้รูปภาพขนาดเต็ม ซึ่งหมายความว่าหากระบบยังไม่ได้โหลดมุมมองใหม่ ผู้ใช้จะเห็นภาพปกตลอดการเปลี่ยน ซึ่งหมายความว่าทรานซิชันจะเริ่มได้ทันทีและรูปภาพขนาดเต็มจะโหลดในเวลาที่เหมาะสม

การดำเนินการนี้จะไม่ได้ผลหากมุมมองใหม่มีความโปร่งใส แต่ในกรณีนี้เราทราบดีว่ามุมมองดังกล่าวไม่มีความโปร่งใส เราจึงเพิ่มประสิทธิภาพได้

จัดการการเปลี่ยนแปลงสัดส่วนการแสดงผล

เพื่อความสะดวก ทรานซิชันทั้งหมดที่ผ่านมาเป็นทรานซิชันขององค์ประกอบที่มีสัดส่วนภาพเดียวกัน แต่ก็ไม่ได้เป็นเช่นนั้นเสมอไป จะเกิดอะไรขึ้นหากภาพปกเป็น 1:1 และรูปภาพหลักเป็น 16:9

องค์ประกอบหนึ่งเปลี่ยนเป็นองค์ประกอบอื่นโดยเปลี่ยนสัดส่วนภาพ การสาธิตแบบจำกัด แหล่งที่มา

ในทรานซิชันเริ่มต้น กลุ่มจะเคลื่อนไหวจากขนาดก่อนไปเป็นขนาดหลัง มุมมองแบบเก่าและแบบใหม่มีความกว้าง 100% ของกลุ่มและความสูงอัตโนมัติ ซึ่งหมายความว่ามุมมองเหล่านี้จะคงอัตราส่วนภาพไว้ ไม่ว่ากลุ่มจะมีขนาดเท่าใดก็ตาม

ซึ่งค่าเริ่มต้นนี้เป็นสิ่งที่ดี แต่ไม่ใช่สิ่งที่ต้องการในกรณีนี้ ดังนั้น

::view-transition-old(full-embed),
::view-transition-new(full-embed) {
  /* Prevent the default animation,
  so both views remain opacity:1 throughout the transition */
  animation: none;
  /* Use normal blending,
  so the new view sits on top and obscures the old view */
  mix-blend-mode: normal;
  /* Make the height the same as the group,
  meaning the view size might not match its aspect-ratio. */
  height: 100%;
  /* Clip any overflow of the view */
  overflow: clip;
}

/* The old view is the thumbnail */
::view-transition-old(full-embed) {
  /* Maintain the aspect ratio of the view,
  by shrinking it to fit within the bounds of the element */
  object-fit: contain;
}

/* The new view is the full image */
::view-transition-new(full-embed) {
  /* Maintain the aspect ratio of the view,
  by growing it to cover the bounds of the element */
  object-fit: cover;
}

ซึ่งหมายความว่าภาพขนาดย่อจะยังคงอยู่ตรงกลางขององค์ประกอบเมื่อความกว้างขยายออก แต่รูปภาพขนาดเต็มจะ "เลิกครอบตัด" เมื่อเปลี่ยนจาก 1:1 เป็น 16:9

ดูข้อมูลโดยละเอียดได้ที่การเปลี่ยนมุมมอง: การจัดการกับการเปลี่ยนแปลงสัดส่วนภาพ


ใช้คิวรีสื่อเพื่อเปลี่ยนทรานซิชันสำหรับสถานะอุปกรณ์ต่างๆ

คุณอาจต้องการใช้ทรานซิชันที่แตกต่างกันในอุปกรณ์เคลื่อนที่กับเดสก์ท็อป เช่น ตัวอย่างนี้ที่ใช้การเลื่อนจากด้านข้างแบบเต็มในอุปกรณ์เคลื่อนที่ แต่ใช้การเลื่อนแบบนุ่มนวลกว่าในเดสก์ท็อป

องค์ประกอบหนึ่งเปลี่ยนเป็นองค์ประกอบอื่น การสาธิตแบบจำกัด แหล่งที่มา

ซึ่งทำได้โดยใช้การค้นหาสื่อทั่วไป ดังนี้

/* Transitions for mobile */
::view-transition-old(root) {
  animation: 300ms ease-out both full-slide-to-left;
}

::view-transition-new(root) {
  animation: 300ms ease-out both full-slide-from-right;
}

@media (min-width: 500px) {
  /* Overrides for larger displays.
  This is the shared axis transition from earlier in the article. */
  ::view-transition-old(root) {
    animation: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
      300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
  }

  ::view-transition-new(root) {
    animation: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in,
      300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
  }
}

นอกจากนี้ คุณอาจต้องเปลี่ยนองค์ประกอบที่จะกําหนด view-transition-name ด้วย โดยขึ้นอยู่กับคําค้นหาสื่อที่ตรงกัน


ตอบสนองต่อค่ากําหนด "การเคลื่อนไหวลดลง"

ผู้ใช้ระบุได้ว่าต้องการลดการเคลื่อนไหวผ่านระบบปฏิบัติการ และค่ากําหนดนี้จะแสดงใน CSS

คุณเลือกป้องกันการเปลี่ยนแปลงสำหรับผู้ใช้เหล่านี้ได้

@media (prefers-reduced-motion) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }
}

อย่างไรก็ตาม ค่ากําหนด "ลดการเคลื่อนไหว" ไม่ได้หมายความว่าผู้ใช้ต้องการไม่มีการเคลื่อนไหว คุณอาจเลือกภาพเคลื่อนไหวที่นุ่มนวลกว่าแทนข้อมูลโค้ดด้านบน แต่ยังคงแสดงความสัมพันธ์ระหว่างองค์ประกอบต่างๆ และขั้นตอนของข้อมูล


จัดการสไตล์การเปลี่ยนมุมมองหลายแบบด้วยประเภทการเปลี่ยนมุมมอง

การรองรับเบราว์เซอร์

  • Chrome: 125
  • Edge: 125
  • Firefox: ไม่รองรับ
  • Safari: 18.

บางครั้งการเปลี่ยนจากมุมมองหนึ่งไปยังอีกมุมมองหนึ่งควรมีทรานซิชันที่ปรับให้เหมาะกับมุมมองนั้นๆ โดยเฉพาะ เช่น เมื่อไปยังหน้าถัดไปหรือหน้าก่อนหน้าในลําดับหน้า คุณอาจต้องการเลื่อนเนื้อหาไปในทิศทางอื่น ทั้งนี้ขึ้นอยู่กับว่าคุณต้องการไปยังหน้าสูงขึ้นหรือหน้าต่ำลงจากลําดับ

ไฟล์บันทึกเสียงการสาธิตการใส่เลขหน้า โดยจะใช้ทรานซิชันที่แตกต่างกันไปตามหน้าที่คุณกำลังจะเข้าชม

คุณสามารถใช้ประเภทการเปลี่ยนมุมมอง ซึ่งช่วยให้คุณกําหนดประเภทอย่างน้อย 1 ประเภทให้กับการเปลี่ยนมุมมองที่ใช้งานอยู่ได้ เช่น เมื่อเปลี่ยนไปที่หน้าสูงขึ้นในลําดับการแบ่งหน้า ให้ใช้ประเภท forwards และเมื่อเปลี่ยนไปที่หน้าล่าง ให้ใช้ประเภท backwards ประเภทเหล่านี้จะทำงานเฉพาะเมื่อจับภาพหรือทำการเปลี่ยนเท่านั้น และคุณปรับแต่งแต่ละประเภทผ่าน CSS เพื่อใช้ภาพเคลื่อนไหวที่แตกต่างกันได้

หากต้องการใช้ประเภทในการเปลี่ยนมุมมองเอกสารเดียวกัน ให้ส่ง types ไปยังเมธอด startViewTransition document.startViewTransition จะยอมรับออบเจ็กต์ด้วยเพื่ออนุญาตการดำเนินการนี้ โดย update คือฟังก์ชัน Callback ที่อัปเดต DOM และ types คืออาร์เรย์ที่มีประเภท

const direction = determineBackwardsOrForwards();

const t = document.startViewTransition({
  update: updateTheDOMSomehow,
  types: ['slide', direction],
});

หากต้องการตอบกลับประเภทเหล่านี้ ให้ใช้ตัวเลือก :active-view-transition-type() ส่ง type ที่ต้องการกําหนดเป้าหมายไปยังตัวเลือก ซึ่งช่วยให้คุณแยกสไตล์ของทรานซิชันของมุมมองหลายรายการออกจากกันได้ โดยที่การประกาศของรายการหนึ่งไม่รบกวนการประกาศของอีกรายการ

เนื่องจากประเภทจะมีผลเฉพาะเมื่อบันทึกหรือทำการเปลี่ยนเท่านั้น คุณจึงใช้ตัวเลือกเพื่อตั้งค่าหรือยกเลิกการตั้งค่า view-transition-name ในองค์ประกอบสำหรับการเปลี่ยนมุมมองที่มีประเภทนั้นๆ เท่านั้น

/* Determine what gets captured when the type is forwards or backwards */
html:active-view-transition-type(forwards, backwards) {
  :root {
    view-transition-name: none;
  }
  article {
    view-transition-name: content;
  }
  .pagination {
    view-transition-name: pagination;
  }
}

/* Animation styles for forwards type only */
html:active-view-transition-type(forwards) {
  &::view-transition-old(content) {
    animation-name: slide-out-to-left;
  }
  &::view-transition-new(content) {
    animation-name: slide-in-from-right;
  }
}

/* Animation styles for backwards type only */
html:active-view-transition-type(backwards) {
  &::view-transition-old(content) {
    animation-name: slide-out-to-right;
  }
  &::view-transition-new(content) {
    animation-name: slide-in-from-left;
  }
}

/* Animation styles for reload type only (using the default root snapshot) */
html:active-view-transition-type(reload) {
  &::view-transition-old(root) {
    animation-name: fade-out, scale-down;
  }
  &::view-transition-new(root) {
    animation-delay: 0.25s;
    animation-name: fade-in, scale-up;
  }
}

ในการสาธิตการแบ่งหน้าต่อไปนี้ เนื้อหาของหน้าจะเลื่อนไปข้างหน้าหรือข้างหลังตามหมายเลขหน้าที่คุณกําลังไปยัง ระบบจะกําหนดประเภทเมื่อมีการคลิกซึ่งระบบจะส่งไปยัง document.startViewTransition

หากต้องการกําหนดเป้าหมายการเปลี่ยนมุมมองที่ใช้งานอยู่ ไม่ว่าประเภทใดก็ตาม ให้ใช้ตัวเลือกคลาสจำลอง :active-view-transition แทน

html:active-view-transition {
    
}

จัดการสไตล์การเปลี่ยนมุมมองหลายสไตล์ด้วยชื่อคลาสที่รูทการเปลี่ยนมุมมอง

บางครั้งการเปลี่ยนจากมุมมองประเภทหนึ่งไปยังอีกประเภทหนึ่งควรมีทรานซิชันที่ปรับให้เหมาะกับแต่ละประเภทโดยเฉพาะ หรือการนำทาง "กลับ" ควรแตกต่างจากการนำทาง "ไปข้างหน้า"

ทรานซิชันที่แตกต่างกันเมื่อ "ย้อนกลับ" การสาธิตแบบจำกัด แหล่งที่มา

ก่อนที่จะมีประเภททรานซิชัน วิธีจัดการกับกรณีเหล่านี้คือการตั้งชื่อคลาสชั่วคราวที่รูททรานซิชัน เมื่อเรียกใช้ document.startViewTransition รูทการเปลี่ยนนี้คือองค์ประกอบ <html> ซึ่งเข้าถึงได้โดยใช้ document.documentElement ใน JavaScript

if (isBackNavigation) {
  document.documentElement.classList.add('back-transition');
}

const transition = document.startViewTransition(() =>
  updateTheDOMSomehow(data)
);

try {
  await transition.finished;
} finally {
  document.documentElement.classList.remove('back-transition');
}

หากต้องการนำคลาสออกหลังจากการเปลี่ยนเสร็จสิ้นแล้ว ตัวอย่างนี้ใช้ transition.finished ซึ่งเป็นสัญญาที่จะดำเนินการเมื่อการเปลี่ยนถึงสถานะสุดท้าย พร็อพเพอร์ตี้อื่นๆ ของออบเจ็กต์นี้จะอยู่ในเอกสารอ้างอิง API

ตอนนี้คุณใช้ชื่อคลาสนั้นใน CSS เพื่อเปลี่ยนทรานซิชันได้แล้ว โดยทำดังนี้

/* 'Forward' transitions */
::view-transition-old(root) {
  animation: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
}

::view-transition-new(root) {
  animation: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in, 300ms
      cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
}

/* Overrides for 'back' transitions */
.back-transition::view-transition-old(root) {
  animation-name: fade-out, slide-to-right;
}

.back-transition::view-transition-new(root) {
  animation-name: fade-in, slide-from-left;
}

เช่นเดียวกับ Media Query การมีคลาสเหล่านี้ยังใช้เพื่อเปลี่ยนองค์ประกอบที่จะได้ view-transition-name ด้วย


เรียกใช้ทรานซิชันโดยไม่ทำให้ภาพเคลื่อนไหวอื่นๆ ค้าง

ดูตัวอย่างตำแหน่งการเปลี่ยนวิดีโอนี้

ทรานซิชันวิดีโอ การสาธิตแบบจำกัด แหล่งที่มา

คุณเห็นปัญหาใดๆ ไหม ไม่ต้องกังวลหากยังไม่ได้ดำเนินการ มาดูวิดีโอที่ชะลอเวลานี้กัน

ทรานซิชันวิดีโอช้าลง การสาธิตแบบจำกัด แหล่งที่มา

ในระหว่างการเปลี่ยน วิดีโอจะดูเหมือนหยุดนิ่ง จากนั้นวิดีโอเวอร์ชันที่เล่นจะค่อยๆ ปรากฏขึ้น เนื่องจาก ::view-transition-old(video) เป็นภาพหน้าจอของมุมมองเก่า ส่วน ::view-transition-new(video) เป็นภาพสดของมุมมองใหม่

คุณแก้ไขเรื่องนี้ได้ แต่ก่อนอื่นให้ถามตัวเองว่าควรเสียเวลาแก้ไขไหม หากคุณไม่เห็น "ปัญหา" เมื่อทรานซิชันเล่นด้วยความเร็วปกติ เราคงไม่เปลี่ยน

หากต้องการแก้ไขจริงๆ ก็ไม่ต้องแสดง ::view-transition-old(video) ให้เปลี่ยนไปใช้ ::view-transition-new(video) เลย ซึ่งทำได้โดยการลบล้างสไตล์และภาพเคลื่อนไหวเริ่มต้น ดังนี้

::view-transition-old(video) {
  /* Don't show the frozen old view */
  display: none;
}

::view-transition-new(video) {
  /* Don't fade the new view in */
  animation: none;
}

เพียงเท่านี้ก็เรียบร้อยแล้ว

ทรานซิชันวิดีโอช้าลง การสาธิตแบบจำกัด แหล่งที่มา

ตอนนี้วิดีโอจะเล่นตลอดช่วงการเปลี่ยนภาพ


การผสานรวมกับ Navigation API (และเฟรมเวิร์กอื่นๆ)

การเปลี่ยนมุมมองจะระบุในลักษณะที่ผสานรวมกับเฟรมเวิร์กหรือไลบรารีอื่นๆ ได้ เช่น หากแอปพลิเคชันหน้าเว็บเดียว (SPA) ใช้เราเตอร์ คุณสามารถปรับกลไกการอัปเดตของเราเตอร์เพื่ออัปเดตเนื้อหาโดยใช้การเปลี่ยนมุมมอง

ในข้อมูลโค้ดต่อไปนี้ซึ่งนำมาจากการสาธิตการแบ่งหน้านี้ ระบบได้ปรับตัวแฮนเดิลการขัดจังหวะของ Navigation API ให้เรียก document.startViewTransition เมื่อระบบรองรับการเปลี่ยนมุมมอง

navigation.addEventListener("navigate", (e) => {
    // Don't intercept if not needed
    if (shouldNotIntercept(e)) return;

    // Intercept the navigation
    e.intercept({
        handler: async () => {
            // Fetch the new content
            const newContent = await fetchNewContent(e.destination.url, {
                signal: e.signal,
            });

            // The UA does not support View Transitions, or the UA
            // already provided a Visual Transition by itself (e.g. swipe back).
            // In either case, update the DOM directly
            if (!document.startViewTransition || e.hasUAVisualTransition) {
                setContent(newContent);
                return;
            }

            // Update the content using a View Transition
            const t = document.startViewTransition(() => {
                setContent(newContent);
            });
        }
    });
});

เบราว์เซอร์บางรุ่น (ไม่ใช่ทุกรุ่น) มีการเปลี่ยนภาพของตนเองเมื่อผู้ใช้ใช้ท่าทางสัมผัสด้วยการปัดเพื่อไปยังส่วนต่างๆ ในกรณีนี้ คุณไม่ควรเรียกใช้การเปลี่ยนมุมมองของคุณเองเนื่องจากจะทำให้ผู้ใช้ได้รับประสบการณ์การใช้งานที่ไม่ดีหรือสับสน ผู้ใช้จะเห็นทรานซิชัน 2 รายการ ซึ่งรายการหนึ่งมาจากเบราว์เซอร์และอีกรายการหนึ่งมาจากคุณ ทำงานต่อเนื่องกัน

ดังนั้น เราขอแนะนำให้ป้องกันไม่ให้การเปลี่ยนมุมมองเริ่มต้นขึ้นเมื่อเบราว์เซอร์มีการเปลี่ยนภาพของตนเอง โดยตรวจสอบค่าของพร็อพเพอร์ตี้ hasUAVisualTransition ของอินสแตนซ์ NavigateEvent ระบบจะตั้งค่าพร็อพเพอร์ตี้เป็น true เมื่อเบราว์เซอร์แสดงการเปลี่ยนภาพ พร็อพเพอร์ตี้ hasUIVisualTransition นี้ยังอยู่ในอินสแตนซ์ PopStateEvent ด้วย

ในข้อมูลโค้ดก่อนหน้า การตรวจสอบที่กําหนดว่าจะเรียกใช้การเปลี่ยนมุมมองหรือไม่จะพิจารณาพร็อพเพอร์ตี้นี้ เมื่อระบบไม่รองรับการเปลี่ยนมุมมองในเอกสารเดียวกัน หรือเมื่อเบราว์เซอร์มีการเปลี่ยนของตัวเองอยู่แล้ว ระบบจะข้ามการเปลี่ยนมุมมอง

if (!document.startViewTransition || e.hasUAVisualTransition) {
  setContent(newContent);
  return;
}

ในวิดีโอต่อไปนี้ ผู้ใช้ปัดเพื่อไปยังหน้าก่อนหน้า ภาพด้านซ้ายไม่ได้ตรวจสอบธง hasUAVisualTransition การบันทึกทางด้านขวามีการตรวจสอบ จึงข้ามการเปลี่ยนมุมมองด้วยตนเองเนื่องจากเบราว์เซอร์มีการเปลี่ยนภาพ

การเปรียบเทียบเว็บไซต์เดียวกันโดยไม่มี (ซ้าย) และการตรวจสอบความกว้าง (ขวา) สำหรับ hasUAVisualTransition
เท่านั้น

การสร้างภาพเคลื่อนไหวด้วย JavaScript

จนถึงตอนนี้ ทรานซิชันทั้งหมดได้รับการกําหนดโดยใช้ CSS แต่บางครั้ง CSS อาจไม่เพียงพอ

การเปลี่ยนแบบวงกลม การสาธิตแบบจำกัด แหล่งที่มา

การเปลี่ยนรูปแบบนี้บางส่วนใช้ CSS เพียงอย่างเดียวไม่ได้

  • ภาพเคลื่อนไหวจะเริ่มจากตำแหน่งที่คลิก
  • ภาพเคลื่อนไหวจะสิ้นสุดลงเมื่อวงกลมมีรัศมีถึงมุมที่ไกลที่สุด แต่หวังว่าจะเป็นไปได้ด้วย CSS ในอนาคต

แต่คุณสร้างทรานซิชันได้โดยใช้ Web Animation API

let lastClick;
addEventListener('click', event => (lastClick = event));

function spaNavigate(data) {
  // Fallback for browsers that don't support this API:
  if (!document.startViewTransition) {
    updateTheDOMSomehow(data);
    return;
  }

  // Get the click position, or fallback to the middle of the screen
  const x = lastClick?.clientX ?? innerWidth / 2;
  const y = lastClick?.clientY ?? innerHeight / 2;
  // Get the distance to the furthest corner
  const endRadius = Math.hypot(
    Math.max(x, innerWidth - x),
    Math.max(y, innerHeight - y)
  );

  // With a transition:
  const transition = document.startViewTransition(() => {
    updateTheDOMSomehow(data);
  });

  // Wait for the pseudo-elements to be created:
  transition.ready.then(() => {
    // Animate the root's new view
    document.documentElement.animate(
      {
        clipPath: [
          `circle(0 at ${x}px ${y}px)`,
          `circle(${endRadius}px at ${x}px ${y}px)`,
        ],
      },
      {
        duration: 500,
        easing: 'ease-in',
        // Specify which pseudo-element to animate
        pseudoElement: '::view-transition-new(root)',
      }
    );
  });
}

ตัวอย่างนี้ใช้ transition.ready ซึ่งเป็นสัญญาที่จะมีผลเมื่อสร้างองค์ประกอบจำลองการเปลี่ยนเรียบร้อยแล้ว พร็อพเพอร์ตี้อื่นๆ ของออบเจ็กต์นี้จะอยู่ในเอกสารอ้างอิง API


ทรานซิชันเป็นการเพิ่มประสิทธิภาพ

View Transition API ออกแบบมาเพื่อ "รวม" การเปลี่ยนแปลง DOM และสร้างการเปลี่ยนผ่าน อย่างไรก็ตาม การเปลี่ยนผ่านควรถือเป็นการเพิ่มประสิทธิภาพ เช่น แอปไม่ควรเข้าสู่สถานะ "ข้อผิดพลาด" หากการเปลี่ยนแปลง DOM สำเร็จ แต่การเปลี่ยนผ่านไม่สำเร็จ โดยปกติแล้วการเปลี่ยนผ่านไม่ควรล้มเหลว แต่หากล้มเหลว ก็ไม่ควรทำให้ประสบการณ์การใช้งานที่เหลือของผู้ใช้เสียหาย

หากต้องการใช้การเปลี่ยนเป็นการปรับปรุง ให้ระวังอย่าใช้ Promise การเปลี่ยนในลักษณะที่จะทำให้แอปแสดงข้อยกเว้นหากการเปลี่ยนไม่สำเร็จ

ไม่ควรทำ
async function switchView(data) {
  // Fallback for browsers that don't support this API:
  if (!document.startViewTransition) {
    await updateTheDOM(data);
    return;
  }

  const transition = document.startViewTransition(async () => {
    await updateTheDOM(data);
  });

  await transition.ready;

  document.documentElement.animate(
    {
      clipPath: [`inset(50%)`, `inset(0)`],
    },
    {
      duration: 500,
      easing: 'ease-in',
      pseudoElement: '::view-transition-new(root)',
    }
  );
}

ปัญหาของตัวอย่างนี้คือ switchView() จะปฏิเสธหากการเปลี่ยนผ่านไม่สามารถเข้าถึงสถานะ ready แต่นั่นไม่ได้หมายความว่ามุมมองจะเปลี่ยนไม่สำเร็จ DOM อาจอัปเดตเรียบร้อยแล้ว แต่มี view-transition-name ซ้ำกัน ระบบจึงข้ามการเปลี่ยน

ให้ดำเนินการต่อไปนี้แทน

ควรทำ
async function switchView(data) {
  // Fallback for browsers that don't support this API:
  if (!document.startViewTransition) {
    await updateTheDOM(data);
    return;
  }

  const transition = document.startViewTransition(async () => {
    await updateTheDOM(data);
  });

  animateFromMiddle(transition);

  await transition.updateCallbackDone;
}

async function animateFromMiddle(transition) {
  try {
    await transition.ready;

    document.documentElement.animate(
      {
        clipPath: [`inset(50%)`, `inset(0)`],
      },
      {
        duration: 500,
        easing: 'ease-in',
        pseudoElement: '::view-transition-new(root)',
      }
    );
  } catch (err) {
    // You might want to log this error, but it shouldn't break the app
  }
}

ตัวอย่างนี้ใช้ transition.updateCallbackDone เพื่อรอการอัปเดต DOM และปฏิเสธหากอัปเดตไม่สำเร็จ switchView จะไม่ปฏิเสธอีกต่อไปหากการเปลี่ยนไม่สำเร็จ แต่จะแก้ไขเมื่อการอัปเดต DOM เสร็จสมบูรณ์ และปฏิเสธหากไม่สำเร็จ

หากต้องการให้ switchView แสดงผลเมื่อมุมมองใหม่ "เสถียร" นั่นคือ การเปลี่ยนภาพเคลื่อนไหวเสร็จสมบูรณ์หรือข้ามไปยังจุดสิ้นสุด ให้แทนที่ transition.updateCallbackDone ด้วย transition.finished


ไม่ใช่ polyfill แต่…

ฟีเจอร์นี้ไม่ใช่ฟีเจอร์ที่โพลีฟีลได้ง่าย อย่างไรก็ตาม ฟังก์ชันตัวช่วยนี้จะช่วยให้การดำเนินการต่างๆ ง่ายขึ้นมากในเบราว์เซอร์ที่ไม่รองรับการเปลี่ยนมุมมอง

function transitionHelper({
  skipTransition = false,
  types = [],
  update,
}) {

  const unsupported = (error) => {
    const updateCallbackDone = Promise.resolve(update()).then(() => {});

    return {
      ready: Promise.reject(Error(error)),
      updateCallbackDone,
      finished: updateCallbackDone,
      skipTransition: () => {},
      types,
    };
  }

  if (skipTransition || !document.startViewTransition) {
    return unsupported('View Transitions are not supported in this browser');
  }

  try {
    const transition = document.startViewTransition({
      update,
      types,
    });

    return transition;
  } catch (e) {
    return unsupported('View Transitions with types are not supported in this browser');
  }
}

และสามารถใช้งานได้ดังนี้

function spaNavigate(data) {
  const types = isBackNavigation ? ['back-transition'] : [];

  const transition = transitionHelper({
    update() {
      updateTheDOMSomehow(data);
    },
    types,
  });

  // …
}

ในเบราว์เซอร์ที่ไม่รองรับการเปลี่ยนมุมมอง ระบบจะยังคงเรียก updateDOM แต่จะไม่มีการเปลี่ยนภาพเคลื่อนไหว

นอกจากนี้ คุณยังระบุ classNames บางส่วนเพื่อเพิ่มลงใน <html> ในระหว่างการเปลี่ยนได้ด้วย ซึ่งจะช่วยให้เปลี่ยนการเปลี่ยนฉากได้ง่ายขึ้นโดยขึ้นอยู่กับประเภทของการนําทาง

นอกจากนี้ คุณยังส่ง true ไปยัง skipTransition ได้หากไม่ต้องการใช้ภาพเคลื่อนไหว แม้ว่าในเบราว์เซอร์ที่รองรับการเปลี่ยนมุมมองก็ตาม ซึ่งมีประโยชน์หากเว็บไซต์ของคุณมีค่ากําหนดของผู้ใช้ให้ปิดใช้ทรานซิชัน


การทำงานกับเฟรมเวิร์ก

หากคุณทํางานกับไลบรารีหรือเฟรมเวิร์กที่แฝงการเปลี่ยนแปลง DOM ไว้ สิ่งที่ยากคือการทำความเข้าใจว่าการเปลี่ยนแปลง DOM เสร็จสมบูรณ์เมื่อใด ต่อไปนี้คือชุดตัวอย่างที่ใช้ตัวช่วยด้านบนในเฟรมเวิร์กต่างๆ

  • React - สิ่งสำคัญคือ flushSync ซึ่งจะใช้ชุดการเปลี่ยนแปลงสถานะแบบซิงค์กัน ใช่ มีคำเตือนที่สำคัญเกี่ยวกับการใช้ API ดังกล่าว แต่ Dan Abramov รับรองว่าเหมาะสมกับกรณีนี้ เช่นเดียวกับ React และโค้ดแบบแอซิงโครนัส เมื่อใช้ Promise ต่างๆ ที่ startViewTransition แสดงผล ให้ตรวจสอบว่าโค้ดทำงานด้วยสถานะที่ถูกต้อง
  • Vue.js - คีย์สําคัญคือ nextTick ซึ่งจะทํางานเมื่อ DOM ได้รับการอัปเดต
  • Svelte - คล้ายกับ Vue มาก แต่วิธีการรอการเปลี่ยนแปลงครั้งถัดไปคือ tick
  • Lit - หัวใจสำคัญคือ Promise this.updateComplete ภายในคอมโพเนนต์ ซึ่งจะดำเนินการเมื่อ DOM ได้รับการอัปเดต
  • Angular - คีย์สําคัญคือ applicationRef.tick ซึ่งจะล้างการเปลี่ยนแปลง DOM ที่รอดําเนินการ ตั้งแต่ Angular เวอร์ชัน 17 เป็นต้นไป คุณสามารถใช้ withViewTransitions ที่มาพร้อมกับ @angular/router ได้

เอกสารอ้างอิง API

const viewTransition = document.startViewTransition(update)

เริ่มViewTransitionใหม่

update คือฟังก์ชันที่เรียกใช้เมื่อบันทึกสถานะปัจจุบันของเอกสารแล้ว

จากนั้นเมื่อ Promise ที่ updateCallback แสดงผลจะเป็นไปตามที่คาดไว้ การเปลี่ยนเฟรมจะเริ่มขึ้นในเฟรมถัดไป หาก Promise ที่ updateCallback แสดงผลปฏิเสธ ระบบจะยกเลิกการเปลี่ยน

const viewTransition = document.startViewTransition({ update, types })

เริ่ม ViewTransition ใหม่ด้วยประเภทที่ระบุ

update จะเรียกใช้เมื่อบันทึกสถานะปัจจุบันของเอกสารแล้ว

types ตั้งค่าประเภทที่ใช้งานอยู่สําหรับการเปลี่ยนเมื่อจับภาพหรือทําการเปลี่ยน โดยช่องนี้จะว่างเปล่าในตอนแรก ดูข้อมูลเพิ่มเติมได้ที่ viewTransition.types ด้านล่าง

สมาชิกอินสแตนซ์ของ ViewTransition

viewTransition.updateCallbackDone

พรอมต์ที่ดำเนินการเมื่อพรอมต์ที่ updateCallback แสดงผลดำเนินการ หรือปฏิเสธเมื่อปฏิเสธ

View Transition API จะรวมการเปลี่ยนแปลง DOM และสร้างการเปลี่ยน อย่างไรก็ตาม บางครั้งคุณอาจไม่สนใจว่าภาพเคลื่อนไหวการเปลี่ยนภาพจะสำเร็จหรือไม่สำเร็จ เพียงแค่ต้องการทราบว่า DOM มีการเปลี่ยนแปลงหรือไม่และเมื่อใด updateCallbackDone นั้นสําหรับกรณีการใช้งานนั้น

viewTransition.ready

พรอมต์ที่ดำเนินการเมื่อสร้างองค์ประกอบจำลองสำหรับทรานซิชันและภาพเคลื่อนไหวกำลังจะเริ่มต้น

ระบบจะปฏิเสธหากการเปลี่ยนแปลงเริ่มต้นไม่ได้ ซึ่งอาจเกิดจากการกําหนดค่าผิดพลาด เช่น view-transition-name ซ้ำกัน หรือหาก updateCallback แสดงผลสัญญาที่ถูกปฏิเสธ

ซึ่งมีประโยชน์สําหรับการสร้างภาพเคลื่อนไหวขององค์ประกอบจำลองการเปลี่ยนด้วย JavaScript

viewTransition.finished

สัญญาที่สำเร็จเมื่อสถานะสุดท้ายปรากฏขึ้นและโต้ตอบกับผู้ใช้ได้อย่างเต็มที่

โดยจะปฏิเสธก็ต่อเมื่อ updateCallback แสดงผลลัพธ์เป็นสัญญาที่ถูกปฏิเสธเท่านั้น เนื่องจากบ่งบอกว่าไม่ได้สร้างสถานะสุดท้าย

มิเช่นนั้น หากการเปลี่ยนสถานะเริ่มต้นไม่สำเร็จหรือข้ามไปในระหว่างการเปลี่ยนสถานะ ระบบจะยังคงไปถึงสถานะสุดท้าย ดังนั้น finished จึงเป็นไปตามข้อกำหนด

viewTransition.types

ออบเจ็กต์ที่คล้ายกับ Set ซึ่งเก็บประเภทของการเปลี่ยนมุมมองที่ใช้งานอยู่ หากต้องการจัดการรายการ ให้ใช้เมธอดอินสแตนซ์ clear(), add() และ delete()

หากต้องการตอบสนองต่อประเภทที่เฉพาะเจาะจงใน CSS ให้ใช้ตัวเลือกคลาสจำลอง :active-view-transition-type(type) ในรูทการเปลี่ยน

ระบบจะล้างข้อมูลประเภทโดยอัตโนมัติเมื่อการเปลี่ยนมุมมองเสร็จสิ้น

viewTransition.skipTransition()

ข้ามส่วนของภาพเคลื่อนไหวในการเปลี่ยน

ซึ่งจะไม่ข้ามการเรียก updateCallback เนื่องจากการเปลี่ยนแปลง DOM แยกต่างหากจากการเปลี่ยน


สไตล์เริ่มต้นและการอ้างอิงการเปลี่ยน

::view-transition
องค์ประกอบจำลองรูทที่เติมเต็มวิวพอร์ตและมี ::view-transition-group แต่ละรายการ
::view-transition-group

ตำแหน่งแบบสัมบูรณ์

การเปลี่ยนสถานะ width และ height ระหว่างสถานะ "ก่อน" และ "หลัง"

การเปลี่ยนtransformระหว่างควอดพื้นที่ในวิวพอร์ต "ก่อน" และ "หลัง"

::view-transition-image-pair

วางตำแหน่งแบบสัมบูรณ์ให้เต็มกลุ่ม

มี isolation: isolate เพื่อจํากัดผลของ mix-blend-mode ในมุมมองเก่าและใหม่

::view-transition-new และ ::view-transition-old

วางตำแหน่งแบบสัมบูรณ์ที่ด้านซ้ายบนของ Wrapper

เติมความกว้างของกลุ่ม 100% แต่มีความสูงอัตโนมัติ จึงจะรักษาสัดส่วนภาพไว้แทนที่จะเติมเต็มกลุ่ม

มี mix-blend-mode: plus-lighter เพื่ออนุญาตการเฟดข้ามอย่างแท้จริง

มุมมองเก่าเปลี่ยนจาก opacity: 1 เป็น opacity: 0 มุมมองใหม่เปลี่ยนจาก opacity: 0 เป็น opacity: 1


ความคิดเห็น

เรายินดีรับฟังความคิดเห็นจากนักพัฒนาแอปเสมอ โดยแจ้งปัญหากับกลุ่มทํางาน CSS ใน GitHub พร้อมคําแนะนําและคําถาม ใส่ [css-view-transitions] ไว้หน้าปัญหา

หากพบข้อบกพร่อง ให้รายงานข้อบกพร่อง Chromium แทน