الوحدة 4 · الحركة والمعمارية
الـ View Transitions
18 دقراءة 4 دبيئة تجريب الكود
الدرس في لمحة
حرّك الانتقال بين حالتين للصفحة نفسها باستخدام document.startViewTransition()، وأعطِ العناصر أسماء لتتحوّل بسلاسة إلى مواضعها الجديدة، وخصّص الحركة أو عطّلها في CSS.
لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.
حين يفعّل زائر Waypoint خيار «Hide sold-out»، تختفي ثلاث بطاقات وتقفز البقية إلى الأعلى لتملأ الفراغات. التغيير فوري وصحيح، لكنه مربك: كانت عينك على البطاقة الرابعة، والآن يقف شيء آخر مكانها. حركة سلسة من التخطيط القديم إلى الجديد كانت ستخبر العين إلى أين ذهبت الأشياء.
تحريك تغييرات التخطيط كان يعني قياس كل عنصر قبل التغيير وبعده وتشغيل حركات FLIP في JavaScript. أما View Transition API فتقوم بالقياس نيابةً عنك.
التقاط، تحديث، تحريك
const button = document.querySelector('#hide-full');
function applyFilter() {
const hide = button.getAttribute('aria-pressed') !== 'true';
button.setAttribute('aria-pressed', String(hide));
for (const card of document.querySelectorAll('.session-card.is-full')) {
card.hidden = hide;
}
}
button.addEventListener('click', () => {
if (!document.startViewTransition) {
applyFilter(); // older browsers: just update
return;
}
document.startViewTransition(() => applyFilter());
});تؤدّي startViewTransition() أربعة أشياء بالترتيب:
- تلتقط صورة (snapshot) للصفحة الحالية.
- تستدعي دالة التحديث لديك، التي تغيّر الـ DOM كيفما شاءت.
- تلتقط الحالة الجديدة.
- تبني شجرة من الـ pseudo-elements تعرض الصورتين القديمة والجديدة، وتحرّك الانتقال بينهما.
الحركة الافتراضية تلاشٍ متقاطع للصفحة كلها. إنها خفيفة، وأفضل من القفزة بالفعل. تحديث الـ DOM نفسه حقيقي وفوري؛ الـ صورة وحدها هي التي تتحرّك، وأثناء تشغيلها تكون الصفحة تحتها في حالتها الجديدة أصلًا. وإن رمى التحديث خطأً أو تُخطّي الانتقال، يظل تغيير الـ DOM يحدث.
سمِّ العناصر لتتحرّك
لتجعل كل بطاقة تنزلق إلى موضعها الجديد، أعطها view-transition-name. كل عنصر مسمّى يحصل على زوج صور خاص به، ويحرّك المتصفّح موضعه وحجمه من القديم إلى الجديد:
.session-card {
view-transition-name: var(--vt-name);
}// Give every card a unique name once, when the list renders
for (const card of document.querySelectorAll('.session-card')) {
card.style.setProperty('--vt-name', `card-${card.id}`);
}يجب أن تكون الأسماء فريدة بين العناصر المرسومة لحظة الالتقاط. البطاقات التي تختفي تتلاشى (فهي موجودة في الصورة القديمة وحدها)، والبطاقات الباقية تنزلق إلى الأعلى لتملأ الفراغات. وعينك تتبعها.
تنسيق الحركة
تعيش الحركة في شجرة من الـ pseudo-elements: ::view-transition في القمة، و::view-transition-group(name) لكل عنصر مسمّى (إضافةً إلى root لبقية الصفحة)، وداخل كلٍّ منها ::view-transition-old(name) و::view-transition-new(name). وتُحرَّك بحركات CSS عادية، فتتحكّم فيها بـ CSS عادية:
::view-transition-group(*) {
animation-duration: 250ms;
animation-timing-function: ease-out;
}
/* Keep the header still while the list moves */
.site-header { view-transition-name: header; }
::view-transition-group(header) { animation: none; }الرمز * يطابق كل المجموعات. ولتقليل الحركة، أزل الحركات ليكون التغيير فوريًا:
@media (prefers-reduced-motion: reduce) {
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
animation: none !important;
}
}لا توجد !important هنا لتهزم حركات المتصفّح الافتراضية؛ فقواعدك العادية تتغلّب عليها أصلًا، كما رأيت في الدرس الأول. إنها موجودة ليتغلّب تفضيل المستخدم أيضًا على أيّ حركة أدقّ كتبتها أنت أو مكتبة ما لمجموعة مسمّاة بعينها، مثل ::view-transition-group(header).
وتستطيع أيضًا استهداف اتجاه واحد من التغيير. الصورة القديمة لبطاقة تختفي ليس لها إلا ::view-transition-old(card-s2)، دون نظير جديد، لذا فإن قاعدة مثل ::view-transition-old(*) { animation-duration: 150ms; } تجعل البطاقات المغادرة تتلاشى أسرع قليلًا من حركة البطاقات الباقية. وفروق صغيرة كهذه تجعل القائمة تبدو منظّمة لا مخلوطة.
انتظار الانتقال
تُرجع startViewTransition() كائن ViewTransition فيه ثلاثة promises. updateCallbackDone يُحسم حين يكتمل تحديثك للـ DOM، وready حين توشك الحركة على البدء، وfinished حين تنتهي وتزول الـ pseudo-elements. استخدم finished لنقل الـ focus أو إعلان نتيجة بعد أن تستقرّ الحركة، مثل إرسال الـ focus إلى أول بطاقة باقية لمستخدمي لوحة المفاتيح. لكن لا تجعل أيّ شيء جوهري ينتظر الحركة: ففي متصفّح بلا هذه الـ API لا يوجد كائن انتقال أصلًا.
دورك الآن
زر الفلتر في التمرين يخفي الجلسات الممتلئة دون حركة. أعطِ كل بطاقة اسمًا فريدًا، وغلّف التحديث في view transition مع بديل احتياطي، واضبط مدة المجموعات على 250ms، وأوقف الحركة لمن يفضّلون تقليلها. تراقب الفحوصات startViewTransition بـ spy وتحذفها أيضًا لتتأكّد من أن بديلك الاحتياطي يعمل. وبعدها ستربط تقدّم الحركة بالتمرير بدل الزمن.
أهم النقاط
document.startViewTransition(update)تلتقط صورة للصفحة، وتشغّل تحديثك للـ DOM، وتلتقط صورة ثانية، ثم تحرّك الانتقال بينهما بتلاشٍ متقاطع (crossfade) افتراضيًا.- العنصر الذي له
view-transition-nameفريد يحصل على زوج صور خاص به، فيتحرّك ويتغيّر حجمه بسلاسة إلى موضعه الجديد بدل أن يتلاشى. - الحركة مبنية من pseudo-elements مثل
::view-transition-group(name)، تنسّقها بحركات CSS عادية. - افحص دائمًا وجود
startViewTransitionوشغّل التحديث مباشرةً حين تكون غائبة، وأزل الحركة للمستخدمين الذين يفضّلون تقليل الحركة.