EduFlow
إتقان CSS الحديثة
⁦0%⁩

الوحدة 4 · الحركة والمعمارية

الـ View Transitions

18 دقراءة 4 دبيئة تجريب الكود

الدرس في لمحة

حرّك الانتقال بين حالتين للصفحة نفسها باستخدام document.startViewTransition()، وأعطِ العناصر أسماء لتتحوّل بسلاسة إلى مواضعها الجديدة، وخصّص الحركة أو عطّلها في CSS.

  1. 1التقاط، تحديث، تحريك
  2. 2سمِّ العناصر لتتحرّك
  3. 3تنسيق الحركة
  4. 4انتظار الانتقال
  5. 5دورك الآن

لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.

حين يفعّل زائر Waypoint خيار «Hide sold-out»، تختفي ثلاث بطاقات وتقفز البقية إلى الأعلى لتملأ الفراغات. التغيير فوري وصحيح، لكنه مربك: كانت عينك على البطاقة الرابعة، والآن يقف شيء آخر مكانها. حركة سلسة من التخطيط القديم إلى الجديد كانت ستخبر العين إلى أين ذهبت الأشياء.

تحريك تغييرات التخطيط كان يعني قياس كل عنصر قبل التغيير وبعده وتشغيل حركات FLIP في JavaScript. أما View Transition API فتقوم بالقياس نيابةً عنك.

التقاط، تحديث، تحريك

filters.js
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() أربعة أشياء بالترتيب:

  1. تلتقط صورة (snapshot) للصفحة الحالية.
  2. تستدعي دالة التحديث لديك، التي تغيّر الـ DOM كيفما شاءت.
  3. تلتقط الحالة الجديدة.
  4. تبني شجرة من الـ pseudo-elements تعرض الصورتين القديمة والجديدة، وتحرّك الانتقال بينهما.
أربع خطوات في صف تصل بينها أسهم: التقاط الصورة القديمة، تشغيل callback التحديث، التقاط الصورة الجديدة، تحريك شجرة الـ pseudo-elements من القديم إلى الجديد. 1. التقاط الحالة القديمة 2. تحديث applyFilter() 3. التقاط الحالة الجديدة 4. تحريك ::view-transition الـ DOM الحقيقي في حالته الجديدة أصلًا أثناء تشغيل الخطوة 4.
يلتقط الـ view transition الحالة القديمة، ويشغّل التحديث، ويلتقط الحالة الجديدة، ثم يحرّك الانتقال

الحركة الافتراضية تلاشٍ متقاطع للصفحة كلها. إنها خفيفة، وأفضل من القفزة بالفعل. تحديث الـ DOM نفسه حقيقي وفوري؛ الـ صورة وحدها هي التي تتحرّك، وأثناء تشغيلها تكون الصفحة تحتها في حالتها الجديدة أصلًا. وإن رمى التحديث خطأً أو تُخطّي الانتقال، يظل تغيير الـ DOM يحدث.

سمِّ العناصر لتتحرّك

لتجعل كل بطاقة تنزلق إلى موضعها الجديد، أعطها view-transition-name. كل عنصر مسمّى يحصل على زوج صور خاص به، ويحرّك المتصفّح موضعه وحجمه من القديم إلى الجديد:

CSS
.session-card {
  view-transition-name: var(--vt-name);
}
JavaScript
// 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 عادية:

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; }

الرمز * يطابق كل المجموعات. ولتقليل الحركة، أزل الحركات ليكون التغيير فوريًا:

CSS
@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 وشغّل التحديث مباشرةً حين تكون غائبة، وأزل الحركة للمستخدمين الذين يفضّلون تقليل الحركة.

قراءات إضافية

تحقّق من فهمك

4 أسئلة

السؤال 1 من 4

استدعيت document.startViewTransition(() => applyFilter()) دون أيّ CSS على الإطلاق. ماذا يرى المستخدم؟

السؤال 2 من 4

لبطاقتين في الصفحة view-transition-name: card كلتيهما. ماذا يحدث حين تبدأ انتقالًا؟

السؤال 3 من 4

كيف تجعل كل view transition في الصفحة تستغرق 250ms؟

السؤال 4 من 4

أيّ عبارة عن الدعم دقيقة في 2026؟

تدريب

بيئة تجريب الكود

حرّك الفلتر بـ view transition

النقر على Hide sold-out يخفي بطاقتين فورًا. اجعل التغيير متحرّكًا:

  1. أعطِ كل .session-card قيمة view-transition-name فريدة (في CSS أو JS أو كليهما).
  2. شغّل تحديث الفلتر داخل document.startViewTransition()، وارجع إلى تحديث عادي حين لا تكون الدالة موجودة.
  3. اجعل كل حركة ::view-transition-group تستغرق 250ms.
  4. أوقف حركات الـ view transition للمستخدمين الذين يفضّلون تقليل الحركة.

تراقب الفحوصات document.startViewTransition بـ spy وتحذفها أيضًا لتختبر بديلك الاحتياطي.

<button id="hide-full" type="button" aria-pressed="false">Hide sold-out</button>
<section class="schedule">
  <article class="session-card" id="s1">Shipping View Transitions</article>
  <article class="session-card is-full" id="s2">Grid Layout Workshop · Sold out</article>
  <article class="session-card" id="s3">Color Spaces for Humans</article>
  <article class="session-card is-full" id="s4">Container Queries Lab · Sold out</article>
  <article class="session-card" id="s5">The Cost of a Frame</article>
</section>جارٍ تحميل المحرّر…

Ctrl/⌘ + Enter للتشغيل · يعمل في بيئة معزولة داخل متصفّحك. لا شيء يغادر جهازك.

المعاينة

الـ console

(لا مخرجات)

الفحوص

  • لكل بطاقة view-transition-name فريد
  • النقر يشغّل التحديث داخل startViewTransition()
  • تُخفى البطاقات الممتلئة وتتحدّث حالة الزر
  • الزر يظل يعمل دون View Transition API
  • مجموعات الـ view transition تستغرق 250ms
  • تُزال الحركة عند تفضيل تقليل الحركة