เอฟเฟกต์พารัลแลกซ์เมื่อเลื่อนเป็นเทคนิคภาพที่เนื้อหาเลเยอร์ต่างๆ เคลื่อนที่ด้วยความเร็วที่แตกต่างกันเมื่อผู้ใช้เลื่อนหน้าลง ซึ่งจะสร้างภาพลวงตาของความลึก โดยองค์ประกอบที่อยู่เบื้องหน้าจะดูเหมือนเคลื่อนที่เร็วกว่าองค์ประกอบที่อยู่เบื้องหลัง ส่งผลให้เกิดประสบการณ์การท่องเว็บที่น่าสนใจและสมจริง วิธีที่ดีที่สุดในการสร้างเอฟเฟกต์นี้คือการใช้ภาพเคลื่อนไหวที่ขับเคลื่อนด้วยการเลื่อนของ CSS ซึ่งช่วยให้คุณลิงก์ภาพเคลื่อนไหวกับตำแหน่งการเลื่อนของคอนเทนเนอร์ได้
วิธีการนำไปใช้งาน
วิธีสร้างเอฟเฟกต์พารัลแลกซ์พื้นฐานมีดังนี้
สร้างองค์ประกอบ Wrapper: องค์ประกอบนี้จะจัดกลุ่มเลเยอร์ทั้งหมดของเอฟเฟกต์พารัลแลกซ์เข้าด้วยกัน องค์ประกอบนี้ไม่ใช่องค์ประกอบที่เลื่อนได้ ดังนั้นจึงควรตัดส่วนที่เกินออก และกำหนด
heightที่ตรงกับความสูงของเลเยอร์ใดเลเยอร์หนึ่งของเอฟเฟกต์พารัลแลกซ์<div class="wrapper"> … </div>.wrapper { overflow: clip; height: 100vh; /* Height of one of the layers of the parallax */ }ประกาศเลเยอร์: เพิ่มเลเยอร์แต่ละรายการที่จะเคลื่อนที่ด้วยความเร็วที่ต่างกันภายใน Wrapper
<div class="wrapper"> <div class="layer">LAYER 0</div> <div class="layer">LAYER 1</div> <div class="layer">LAYER 2</div> … </div>เพิ่มภาพเคลื่อนไหวแบบแปล: กำหนดภาพเคลื่อนไหว CSS ที่เปลี่ยนพร็อพเพอร์ตี้
transformของเลเยอร์ สำหรับเอฟเฟกต์พารัลแลกซ์ โดยทั่วไปคุณจะใช้translateYเพื่อย้ายเลเยอร์ในแนวตั้ง@keyframes parallax { from { transform: translateY(700px); } }ตั้งค่า
view-timeline: หากต้องการลิงก์ภาพเคลื่อนไหวกับตำแหน่งการเลื่อน ให้สร้างview-timelineในองค์ประกอบ Wrapper แล้วนำไปใช้กับเลเยอร์.wrapper { view-timeline: --wrapper; } .layer { animation: parallax linear both; animation-timeline: --wrapper; }สลับเวลาของภาพเคลื่อนไหว: หากต้องการให้เลเยอร์เคลื่อนไหวด้วยความเร็วที่ต่างกัน คุณสามารถใช้วิธีหลัก 2 วิธี ได้แก่ สลับเวลาในคีย์เฟรม หรือสลับเวลาของ
animation-rangeทั้ง 2 แนวทางนี้สามารถใช้ค่าที่ฮาร์ดโค้ดหรือใช้การติดตั้งใช้งาน
sibling-index()/sibling-count()ก็ได้ ค่าที่ฮาร์ดโค้ดเป็นค่าที่ง่ายที่สุดและยังมีประโยชน์เมื่อมีเลเยอร์จำนวนจำกัดเท่านั้น การติดตั้งใช้งานsibling-index()/sibling-count()จะมีประโยชน์เมื่อคุณมีหลายเลเยอร์การจัดเรียงคีย์เฟรมแบบเหลื่อมกัน
เมื่อใช้ค่าที่ฮาร์ดโค้ด คุณจะกำหนดคุณสมบัติที่กำหนดเองสำหรับแต่ละเลเยอร์เพื่อควบคุมออฟเซ็ตพารัลแลกซ์ด้วยตนเองได้
.layer:nth-child(1) { --offset: 100px; } .layer:nth-child(2) { --offset: 200px; } .layer:nth-child(3) { --offset: 300px; } @keyframes parallax { from { transform: translateY(var(--offset)); } }เมื่อใช้
sibling-index()ให้ฟังก์ชันsibling-index()แสดงผลดัชนีขององค์ประกอบย่อยในบรรดาองค์ประกอบระดับเดียวกันเพื่อคำนวณเอฟเฟกต์แบบสลับกันโดยอัตโนมัติ@keyframes parallax { from { transform: translateY(calc(100px * sibling-index())); } }การจัดเรียง
animation-range:เมื่อใช้ค่าที่ฮาร์ดโค้ด คุณจะกำหนดขอบเขตของ
animation-rangeในแต่ละเลเยอร์แยกกันได้อย่างชัดเจน.layer:nth-child(1) { animation-range: entry 25% exit 50%; } .layer:nth-child(2) { animation-range: entry 25% exit 75%; } .layer:nth-child(3) { animation-range: entry 25% exit 100%; }เมื่อใช้
sibling-index()และsibling-count()คุณจะคำนวณช่วงทางคณิตศาสตร์ได้โดยอิงตามจำนวนเลเยอร์ทั้งหมด (sibling-count()).layer { animation-range: entry 25% exit calc(100% / sibling-count() * sibling-index()); }
โค้ดตัวอย่าง
@keyframes parallax {
from {
transform: translateY(calc(100px * sibling-index()));
}
}
.wrapper {
view-timeline: --wrapper;
}
.layer {
animation: parallax linear both;
animation-timeline: --wrapper;
}
@media (prefers-reduced-motion: reduce) {
.layer {
animation: none;
}
}
หรือจะใช้พร็อพเพอร์ตี้ animation-range เพื่อให้ได้ผลลัพธ์ที่คล้ายกันก็ได้ ดังนี้
@keyframes parallax {
from {
transform: translateY(700px);
}
}
.wrapper {
view-timeline: --wrapper;
}
.layer {
animation: parallax linear both;
animation-timeline: --wrapper;
animation-range: entry 25% exit calc(100% / sibling-count() * sibling-index());
}
@media (prefers-reduced-motion: reduce) {
.layer {
animation: none;
}
}
แนวทางปฏิบัติแนะนำ
เมื่อใช้ภาพเคลื่อนไหวที่ขับเคลื่อนด้วยการเลื่อน คุณควรทำตามแนวทางปฏิบัติแนะนำต่อไปนี้เพื่อให้ผู้ใช้ได้รับประสบการณ์การใช้งานที่ราบรื่นและเข้าถึงได้
- ควรรวมการตรวจหาฟีเจอร์: เบราว์เซอร์บางรายการไม่รองรับภาพเคลื่อนไหวที่ขับเคลื่อนด้วยการเลื่อน ใช้
@supports ((animation-timeline: view()) and (animation-range: entry))เพื่อตรวจสอบการรองรับและระบุตัวเลือกสำรองสำหรับเบราว์เซอร์ที่ไม่รองรับ- ต้องรวมการตรวจสอบ
(animation-range: entry)ไว้ที่นี่เพื่อกรองเบราว์เซอร์ที่รองรับเพียงบางส่วนออก - อย่าใช้แพ็กเกจ
scroll-timeline-polyfillสำหรับกลยุทธ์สำรอง เนื่องจากฟีเจอร์ยังไม่สมบูรณ์และมีปัญหาที่ทราบอยู่แล้วจำนวนมาก - หากภาพเคลื่อนไหวถือเป็นเพียงการตกแต่ง ให้เลือกใช้การเพิ่มประสิทธิภาพแบบต่อเนื่อง และอย่าระบุทางเลือกสำรอง
- ต้องรวมการตรวจสอบ
- ควรเคารพค่ากำหนดของผู้ใช้: ผู้ใช้บางรายต้องการให้มีการเคลื่อนไหวน้อยลงบนเว็บ ใช้คิวรีสื่อ
prefers-reduced-motionเพื่อปิดใช้หรือลดภาพเคลื่อนไหวสำหรับผู้ใช้เหล่านี้ - ควรพยายามทำให้เฉพาะพร็อพเพอร์ตี้ CSS ที่มีประสิทธิภาพเคลื่อนไหว: เพื่อให้ภาพเคลื่อนไหวราบรื่นที่สุด ให้ใช้พร็อพเพอร์ตี้ที่เคลื่อนไหวซึ่งจัดการได้โดยเทรด Compositor ของเบราว์เซอร์ เช่น
transformและopacityการทำให้พร็อพเพอร์ตี้อื่นๆ เช่นwidthหรือheightเคลื่อนไหวอาจทำให้เกิดปัญหาด้านประสิทธิภาพ - ควรใช้ลำดับการประกาศที่ถูกต้อง: เมื่อใช้พร็อพเพอร์ตี้ชวเลข
animationให้ประกาศanimation-timelineและanimation-rangeหลังจากพร็อพเพอร์ตี้ดังกล่าวเพื่อป้องกันไม่ให้ชวเลขรีเซ็ตไทม์ไลน์
สำหรับการตั้งค่า animation-range ให้ทำดังนี้
- ควรกำหนดให้เลเยอร์ทั้งหมดมีออฟเซ็ตเริ่มต้นเดียวกัน เช่น
entry 25% - ควรกำหนดค่าออฟเซ็ตสิ้นสุดที่แตกต่างกันให้กับเลเยอร์ทั้งหมดที่ใช้
sibling-count()และsibling-index()เพื่อกระจายค่าออฟเซ็ต เช่นexit calc(100% / sibling-count() * sibling-index())
การรองรับเบราว์เซอร์และกลยุทธ์สำรอง
ดังนั้น โดยทั่วไปจึงต้องมีกลยุทธ์สำรอง
สำหรับเบราว์เซอร์ที่ไม่รองรับภาพเคลื่อนไหวที่ขับเคลื่อนด้วยการเลื่อน คุณสามารถใช้โหมดสำรองเพื่อสร้างเอฟเฟกต์ภาพขึ้นมาใหม่ได้ โดยปกติแล้ว ฟังก์ชันสำรองจะสร้างขึ้นด้วย Listener การเลื่อน (สำหรับเอฟเฟกต์ ScrollTimeline) หรือ IntersectionObserver API (สำหรับเอฟเฟกต์ ViewTimeline)
ในเบราว์เซอร์ที่มีการรองรับภาพเคลื่อนไหวที่ขับเคลื่อนด้วยการเลื่อนในตัว ให้ใช้การติดตั้งใช้งาน CSS แบบเนทีฟเสมอ เนื่องจากมีประสิทธิภาพมากกว่า
โปรดทราบว่าคุณไม่สามารถสร้างเอฟเฟกต์ทั้งหมดขึ้นมาใหม่ได้โดยใช้วิธีการสำรอง
สำหรับ Use Case นี้โดยเฉพาะ สคริปต์ต่อไปนี้จะใช้โหมดสำรองสำหรับเบราว์เซอร์ที่ไม่รองรับภาพเคลื่อนไหวที่ขับเคลื่อนด้วยการเลื่อน โดยใช้ IntersectionObserver เพื่อติดตามระดับการมองเห็นขององค์ประกอบ .wrapper และอัปเดตพร็อพเพอร์ตี้ transform ของเลเยอร์ตามตำแหน่งการเลื่อน
// Fallback for browsers that don't support scroll-driven animations
if (!CSS.supports('(animation-timeline: view()) and (animation-range: entry)')) {
const wrapper = document.querySelector('.wrapper');
const layers = document.querySelectorAll('.layer');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
window.addEventListener('scroll', onScroll);
} else {
window.removeEventListener('scroll', onScroll);
}
});
}, { threshold: 0 });
observer.observe(wrapper);
function onScroll() {
const scrollY = window.scrollY;
const wrapperRect = wrapper.getBoundingClientRect();
const wrapperTop = wrapperRect.top + scrollY;
const wrapperHeight = wrapperRect.height;
const windowHeight = window.innerHeight;
if (scrollY >= wrapperTop - windowHeight && scrollY <= wrapperTop + wrapperHeight) {
const scrollPercent = (scrollY - (wrapperTop - windowHeight)) / (wrapperHeight + windowHeight);
layers.forEach((layer, index) => {
// This matches the effect as defined in the CSS example above.
// Customize this further if needed.
const initialTranslateY = 100 * index;
const translateY = initialTranslateY * (1 - scrollPercent);
layer.style.transform = `translateY(${translateY}px)`;
});
}
}
// Trigger onScroll once to set initial positions
onScroll();
}