View Transition API ช่วยให้การเปลี่ยน DOM เป็นเรื่องง่ายในขั้นตอนเดียว พร้อมกับสร้างการเปลี่ยนแบบเคลื่อนไหวระหว่าง 2 สถานะ โดยพร้อมใช้งานใน Chrome 111 ขึ้นไป
ทำไมเราถึงต้องมีฟีเจอร์นี้
การเปลี่ยนหน้าไม่เพียงแค่ดูดีเท่านั้น แต่ยังสื่อสารทิศทางของความลื่นไหล และทำให้องค์ประกอบต่างๆ เกี่ยวข้องกันอย่างชัดเจนในแต่ละหน้า นอกจากนี้ยังเกิดขึ้นได้ในระหว่างการดึงข้อมูล ทำให้เห็นภาพประสิทธิภาพได้เร็วขึ้น
แต่เรามีเครื่องมือภาพเคลื่อนไหวบนเว็บอยู่แล้ว เช่น การเปลี่ยน CSS, ภาพเคลื่อนไหวของ CSS และ Web Animation API เราจึงต้องใช้สิ่งใหม่เพื่อย้ายสิ่งต่างๆ ด้วย
ความจริงก็คือ การเปลี่ยนสถานะเป็นเรื่องยาก แม้แต่ด้วยเครื่องมือที่เรามีอยู่แล้ว
แม้แต่ฟังก์ชันการเฟดแบบง่ายๆ ก็เกี่ยวข้องกับทั้ง 2 สถานะที่แสดงพร้อมกัน ซึ่งส่งผลให้เกิดความท้าทายด้านความสามารถในการใช้งาน เช่น การจัดการการโต้ตอบเพิ่มเติมในองค์ประกอบขาออก นอกจากนี้ สำหรับผู้ใช้อุปกรณ์อำนวยความสะดวก จะมีช่วงหนึ่งที่ทั้งสถานะก่อนและหลังจะอยู่ใน DOM พร้อมกัน และสิ่งต่างๆ อาจเคลื่อนที่ไปรอบๆ ต้นไม้ในลักษณะที่สวยงาม แต่อาจทำให้ตำแหน่งการอ่านและโฟกัสหายไปได้ง่ายๆ
การจัดการกับการเปลี่ยนแปลงสถานะจะทําได้ยากหากทั้ง 2 สถานะมีตําแหน่งการเลื่อนแตกต่างกัน และหากองค์ประกอบจะย้ายจากคอนเทนเนอร์หนึ่งไปยังอีกคอนเทนเนอร์หนึ่ง คุณอาจพบปัญหาในการตัดออก overflow: hidden
และการตัดคลิปรูปแบบอื่นๆ ซึ่งหมายความว่าคุณต้องปรับโครงสร้าง CSS ใหม่เพื่อให้ได้ผลลัพธ์ที่ต้องการ
นั่นเป็นไปไม่ได้ เพียงแต่ยากมากๆ
ส่วน "ดูการเปลี่ยน" ทำได้ง่ายขึ้น โดยให้คุณทำการเปลี่ยนแปลง DOM ได้โดยไม่เกิดการทับซ้อนกันระหว่างสถานะ แต่ให้สร้างภาพเคลื่อนไหวการเปลี่ยนระหว่างสถานะโดยใช้มุมมองสแนปชอต
นอกจากนี้ แม้ว่าปัจจุบันการใช้งานจะกำหนดเป้าหมายไปที่แอปหน้าเว็บเดียว (SPA) แต่ฟีเจอร์นี้จะมีการขยายการให้บริการเพื่อให้มีการเปลี่ยนระหว่างการโหลดหน้าเว็บแบบเต็ม ซึ่งปัจจุบันทำไม่ได้
สถานะมาตรฐาน
ฟีเจอร์นี้อยู่ระหว่างการพัฒนาในคณะทำงาน CSS ของ W3C ในฐานะข้อกำหนดฉบับร่าง
เมื่อพอใจกับการออกแบบ API แล้ว เราจะเริ่มกระบวนการและการตรวจสอบที่จำเป็นในการจัดส่งฟีเจอร์นี้ไปยังเวอร์ชันเสถียร
ความคิดเห็นของนักพัฒนาแอปเป็นสิ่งสำคัญมาก ดังนั้นโปรดแจ้งปัญหาใน GitHub พร้อมคำแนะนำและคำถาม
การเปลี่ยนที่ง่ายที่สุด: การเฟดแบบต่อเนื่อง
การเปลี่ยน View เริ่มต้นเป็นแบบจางลง ดังนั้นจึงเป็นข้อมูลเบื้องต้นที่ดีเกี่ยวกับ 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;
}
การเปลี่ยนแปลงเพียงครั้งเดียวทำให้การค่อยๆ จางลงช้าลงมาก:
โอเค ยังไม่น่าประทับใจ ลองใช้การเปลี่ยนแกนที่ใช้ร่วมกันของ 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
ซึ่งหมายความว่าจะไม่มีชื่อการเปลี่ยน) ซึ่งใช้เพื่อระบุองค์ประกอบในการเปลี่ยนแปลงอย่างไม่ซ้ำกัน
ผลลัพธ์ที่ได้คือ
ตอนนี้ส่วนหัวจะอยู่ที่เดิมและจางลง
การประกาศ 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 กลุ่ม อันหนึ่งสำหรับส่วนหัวและอีกจุดหนึ่งสำหรับที่เหลือ ซึ่งสามารถกำหนดเป้าหมายได้อย่างอิสระด้วย 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)
└─ …
แต่จะใช้ค่าเริ่มต้นดังนี้
ตอนนี้ข้อความหัวข้อดูน่าพอใจเล็กน้อยจนสามารถเลื่อนผ่านเพื่อเพิ่มพื้นที่สำหรับปุ่มย้อนกลับ
การแก้ไขข้อบกพร่องของการเปลี่ยน
เนื่องจากการเปลี่ยนมุมมองสร้างขึ้นจากภาพเคลื่อนไหว CSS แผงภาพเคลื่อนไหวในเครื่องมือสำหรับนักพัฒนาเว็บใน 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();
});
};
แล้วผลลัพธ์ที่ได้ก็คือ
ตอนนี้ภาพขนาดย่อจะเปลี่ยนเป็นรูปภาพหลัก แม้ว่าองค์ประกอบทั้งสองจะมีแนวคิด (และจริงๆ ก็ตาม) ต่างกัน แต่ 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;
}
}
อย่างไรก็ตาม การตั้งค่า "ลดการเคลื่อนไหว" ไม่ได้หมายความว่าผู้ใช้ต้องการไม่มีการเคลื่อนไหว แทนที่จะตัวเลือกด้านบน คุณอาจเลือกภาพเคลื่อนไหวแบบละเอียดยิ่งขึ้น แต่ยังคงแสดงความสัมพันธ์ระหว่างองค์ประกอบและการไหลของข้อมูล
การเปลี่ยนช่วงการเปลี่ยนตามประเภทของการนำทาง
บางครั้งการนำทางจากหน้าประเภทหนึ่งไปอีกหน้าหนึ่งควรมีการเปลี่ยนผ่านการปรับแต่งให้เหมาะสม หรือการนำทาง "กลับ" ควรแตกต่างจากการนำทาง "ไปข้างหน้า"
วิธีที่ดีที่สุดในการจัดการกรณีเหล่านี้คือการตั้งชื่อคลาสใน <html>
หรือที่เรียกอีกอย่างว่าองค์ประกอบเอกสาร
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;
}
เช่นเดียวกับคำค้นหาสื่อ การมีอยู่ของคลาสเหล่านี้อาจใช้เพื่อเปลี่ยนองค์ประกอบที่จะได้รับ 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;
}
เพียงเท่านี้ก็เรียบร้อยแล้ว
ตอนนี้วิดีโอจะเล่นตลอดการเปลี่ยนผ่าน
สร้างภาพเคลื่อนไหวด้วย JavaScript
จนถึงตอนนี้ การเปลี่ยนทั้งหมดได้รับการกำหนดโดยใช้ CSS แต่บางครั้ง CSS ก็ยังไม่เพียงพอ
การเปลี่ยนแปลงนี้มี 2 - 3 ส่วนที่ไม่สามารถทำได้หากใช้ 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 สำเร็จ แต่การเปลี่ยนล้มเหลว ตามหลักการแล้ว การเปลี่ยนไม่ควรล้มเหลว แต่ถ้าเป็นเช่นนั้น ก็ไม่ควรทำให้ประสบการณ์ของผู้ใช้ที่เหลือเสียหาย
หากต้องการถือว่าการเปลี่ยนนั้นเป็นการเพิ่มประสิทธิภาพ โปรดอย่าใช้สัญญาการเปลี่ยนในลักษณะที่จะทำให้แอปของคุณแสดงผลหากการเปลี่ยนนั้นล้มเหลว
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,
classNames = [],
updateDOM,
}) {
if (skipTransition || !document.startViewTransition) {
const updateCallbackDone = Promise.resolve(updateDOM()).then(() => {});
return {
ready: Promise.reject(Error('View transitions unsupported')),
updateCallbackDone,
finished: updateCallbackDone,
skipTransition: () => {},
};
}
document.documentElement.classList.add(...classNames);
const transition = document.startViewTransition(updateDOM);
transition.finished.finally(() =>
document.documentElement.classList.remove(...classNames)
);
return transition;
}
และนำมาใช้ได้ดังนี้
function spaNavigate(data) {
const classNames = isBackNavigation ? ['back-transition'] : [];
const transition = transitionHelper({
classNames,
updateDOM() {
updateTheDOMSomehow(data);
},
});
// …
}
ในเบราว์เซอร์ที่ไม่รองรับ "ดูการเปลี่ยน" ระบบจะยังคงเรียกใช้ updateDOM
แต่จะไม่มีการเปลี่ยนเป็นภาพเคลื่อนไหว
นอกจากนี้ คุณยังระบุclassNames
บางส่วนเพื่อเพิ่มลงใน <html>
ในช่วงการเปลี่ยนผ่านได้ ซึ่งจะช่วยให้เปลี่ยนการเปลี่ยนผ่านตามประเภทของการนำทางได้ง่ายขึ้น
นอกจากนี้ คุณยังส่งผ่าน true
ไปยัง skipTransition
ได้หากไม่ต้องการภาพเคลื่อนไหว แม้แต่ในเบราว์เซอร์ที่รองรับการเปลี่ยนแบบดูก็ตาม วิธีนี้มีประโยชน์หากเว็บไซต์ของคุณต้องการให้ผู้ใช้ปิดใช้งานการเปลี่ยน
การทำงานกับเฟรมเวิร์ก
หากคุณกำลังทำงานกับไลบรารีหรือเฟรมเวิร์กที่แยกการเปลี่ยนแปลง DOM ออก ส่วนที่ยากคือรู้ว่าการเปลี่ยนแปลง DOM เสร็จสมบูรณ์เมื่อใด ต่อไปนี้คือตัวอย่างชุดตัวอย่างซึ่งใช้ตัวช่วยข้างต้นในเฟรมเวิร์กต่างๆ
- รีแอค - คีย์ในที่นี้คือ
flushSync
ซึ่งจะใช้ชุดการเปลี่ยนแปลงสถานะแบบพร้อมกัน ใช่ เรามีคำเตือนสำคัญเกี่ยวกับการใช้ API นั้น แต่ Dan Abramov ยืนยันว่าเหมาะสมสำหรับกรณีนี้ เช่นเคยสำหรับ React และโค้ดอะซิงโครนัส เมื่อใช้สัญญาต่างๆ ที่startViewTransition
แสดงผล โปรดตรวจสอบว่าโค้ดกำลังทำงานด้วยสถานะที่ถูกต้อง - Vue.js - คีย์ในที่นี้คือ
nextTick
ซึ่งจะทํางานเมื่อ DOM ได้รับการอัปเดตแล้ว - Svelte - คล้ายกับ Vue มาก แต่วิธีการที่จะรอการเปลี่ยนแปลงครั้งถัดไปคือ
tick
- Lit - คีย์ในที่นี้คือ
this.updateComplete
คำมั่นสัญญาภายในคอมโพเนนต์ ซึ่งจะดำเนินการอีกครั้งเมื่อ DOM ได้รับการอัปเดตแล้ว - Angular - คีย์ในที่นี้คือ
applicationRef.tick
ซึ่งจะล้างการเปลี่ยนแปลง DOM ที่รอดำเนินการ ตั้งแต่ Angular เวอร์ชัน 17 เป็นต้นไป คุณสามารถใช้withViewTransitions
ที่มาพร้อมกับ@angular/router
ได้
เอกสารอ้างอิง API
const viewTransition = document.startViewTransition(updateCallback)
เริ่ม
ViewTransition
ใหม่updateCallback
จะถูกเรียกเมื่อบันทึกสถานะปัจจุบันของเอกสารแล้วจากนั้น เมื่อ
updateCallback
ตอบกลับสิ่งที่สัญญาไว้จะทำได้สำเร็จ การเปลี่ยนจะเริ่มในเฟรมถัดไป หากคำมั่นสัญญาที่updateCallback
ส่งกลับมาถูกปฏิเสธ การเปลี่ยนผ่านจะถูกยกเลิก
สมาชิกอินสแตนซ์ของ ViewTransition
:
viewTransition.updateCallbackDone
สัญญาที่จะบรรลุผลเมื่อสัญญาที่ส่งกลับมาโดย
updateCallback
ตอบสนองหรือถูกปฏิเสธเมื่อปฏิเสธView Transition API จะรวมการเปลี่ยนแปลง DOM และสร้างการเปลี่ยน อย่างไรก็ตาม บางครั้งคุณอาจไม่สนใจถึงความสำเร็จ/ความล้มเหลวของภาพเคลื่อนไหวการเปลี่ยน คุณแค่อยากรู้ว่าการเปลี่ยนแปลง DOM จะเกิดขึ้นหรือไม่และเมื่อใด
updateCallbackDone
มีไว้สำหรับกรณีการใช้งานดังกล่าวviewTransition.ready
คำสัญญาที่จะมีผลเมื่อมีการสร้างองค์ประกอบเทียมสำหรับการเปลี่ยน และภาพเคลื่อนไหวกำลังจะเริ่มต้น
และปฏิเสธหากการเปลี่ยนไม่สามารถเริ่มต้นได้ ซึ่งอาจเป็นเพราะการกำหนดค่าที่ไม่ถูกต้อง เช่น
view-transition-name
ที่ซ้ำกัน หรือหากupdateCallback
ส่งคำสัญญาที่ถูกปฏิเสธกลับมาวิธีนี้มีประโยชน์สำหรับการทำให้องค์ประกอบเทียมของการเปลี่ยนเคลื่อนไหวด้วย JavaScript
viewTransition.finished
คำมั่นสัญญาที่จะบรรลุผลเมื่อสถานะสิ้นสุดจะแสดงและโต้ตอบกับผู้ใช้ได้อย่างเต็มที่
จะปฏิเสธก็ต่อเมื่อ
updateCallback
แสดงผลสัญญาที่ถูกปฏิเสธ เนื่องจากข้อความนี้บ่งบอกว่าไม่มีการสร้างสถานะสิ้นสุดมิฉะนั้น หากการเปลี่ยนเริ่มต้นไม่สําเร็จหรือข้ามในระหว่างการเปลี่ยน สถานะสิ้นสุดจะยังคงเป็นสถานะสุดท้าย ดังนั้น
finished
จะดําเนินการต่อviewTransition.skipTransition()
ข้ามส่วนของภาพเคลื่อนไหวในการเปลี่ยน
การดำเนินการนี้จะไม่ข้ามการเรียกใช้
updateCallback
เนื่องจากการเปลี่ยนแปลง DOM จะแยกต่างหากจากการเปลี่ยน
รูปแบบเริ่มต้นและการอ้างอิงการเปลี่ยน
::view-transition
- องค์ประกอบจำลองระดับรูทซึ่งเติมลงในวิวพอร์ตและมี
::view-transition-group
แต่ละรายการ ::view-transition-group
วางตำแหน่งอย่างเหมาะเจาะ
การเปลี่ยน
width
และheight
ระหว่างสถานะ "ก่อน" และ "หลัง"การเปลี่ยน
transform
ระหว่างสี่เหลี่ยมจัตุรัสในวิวพอร์ต "ก่อน" และ "หลัง"::view-transition-image-pair
ตำแหน่งที่ดีที่สุดในการเติมเต็มกลุ่ม
มี
isolation: isolate
เพื่อจำกัดผลกระทบของโหมดผสานplus-lighter
ในมุมมองเก่าและใหม่::view-transition-new
และ::view-transition-old
วางตำแหน่งสัมบูรณ์ไว้ที่ด้านซ้ายบนของ Wrapper
ใช้ความกว้างกลุ่ม 100% แต่มีความสูงอัตโนมัติ จึงจะคงสัดส่วนภาพไว้แทนการเติมข้อมูลกลุ่ม
มี
mix-blend-mode: plus-lighter
เพื่อทำให้เกิดการจางลงอย่างแท้จริงมุมมองเดิมเปลี่ยนจาก
opacity: 1
เป็นopacity: 0
มุมมองใหม่จะเปลี่ยนจากopacity: 0
เป็นopacity: 1
ความคิดเห็น
ความคิดเห็นของนักพัฒนาแอปมีความสำคัญมากในขั้นตอนนี้ ดังนั้นโปรดส่งปัญหาเกี่ยวกับ GitHub พร้อมคำแนะนำและคำถาม