انتقالات عرض المستند نفسه لتطبيقات الصفحة الواحدة

تاريخ النشر: 17 آب (أغسطس) 2021، تاريخ آخر تعديل: 25 أيلول (سبتمبر) 2024

عند تشغيل انتقال عرض على مستند واحد، يطلق عليه انتقال عرض المستند نفسه. ينطبق هذا الأمر عادةً في تطبيقات الصفحة الواحدة (SPA) التي يتم فيها استخدام JavaScript لتعديل نموذج العناصر في المستند (DOM). أصبحت عمليات النقل بين طرق عرض المستند نفسه متاحة في 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());
}

عند الاستدعاء، يلتقط المتصفّح تلقائيًا لقطات لجميع العناصر التي تتضمّن سمة CSS view-transition-name تم تعريفها عليها.

بعد ذلك، تنفّذ الدالة التي تم تمريرها في معاودة الاتصال التي تعدِّل DOM، وبعدها تأخذ لقطات للحالة الجديدة.

ثم يتم ترتيب هذه اللقطات في شجرة من العناصر الزائفة ثم يتم تحريكها باستخدام قوة الصور المتحركة في CSS. يتم نقل أزواج من اللقطات من الحالة القديمة والحالة الجديدة بسلاسة من موضعهما وحجمهما القديمَين إلى موقعهما الجديد، بينما يتم تمويه محتوى اللقطات. إذا كنت ترغب في ذلك، يمكنك استخدام CSS لتخصيص الرسوم المتحركة.


الانتقال التلقائي: تلاشي متقاطع

انتقال العرض التلقائي هو انتقال مموّه، لذا فهو يشكّل مقدّمة رائعة لواجهة برمجة التطبيقات:

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()، تسجِّل واجهة برمجة التطبيقات الحالة الحالية للصفحة. ويشمل ذلك أخذ لقطة شاشة.

بعد اكتمال العملية، يتم استدعاء دالة الاستدعاء التي تم تمريرها إلى .startViewTransition(). وهذا هو المكان الذي يتم فيه تغيير DOM. بعد ذلك، تسجِّل واجهة برمجة التطبيقات الحالة الجديدة للصفحة.

بعد تسجيل الحالة الجديدة، تنشئ واجهة برمجة التطبيقات شجرة عناصر زائفة على النحو التالي:

::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) هو تمثيل مباشر لطريقة العرض الجديدة. ويتم عرض كليهما على أنّهما "محتوى تم استبداله" في 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)

تتوفّر الآن مجموعتَان للانتقال. أحدهما للعنوان والآخر للمحتوى. ويمكن استهدافها بشكل مستقل باستخدام CSS، وتقديم انتقالات مختلفة لها. ومع ذلك، في هذه الحالة، تم استخدام الانتقال التلقائي، وهو انتقال محوّل.main-header

حسنًا، الانتقال التلقائي ليس مجرد تلاشي متقاطع، بل انتقالات ::view-transition-group أيضًا:

  • الموضع والتحويل (باستخدام transform)
  • العرض
  • الطول

لم يكن هذا الأمر مهمًا حتى الآن، لأنّ العنوان له الحجم والموقع نفسهما على جانبَي تغيير DOM. ولكن يمكنك أيضًا استخراج النص في العنوان:

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

يُستخدَم الرمز fit-content لكي يكون حجم العنصر هو حجم النص، بدلاً من التمديد إلى العرض المتبقّي. بدون هذا الإجراء، يقلّل السهم المتّجه للخلف حجم عنصر نص العنوان، بدلاً من الحجم نفسه في كلتا الصفحتَين.

لدينا الآن ثلاثة أجزاء يمكننا الاستفادة منها:

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

لكن مرة أخرى، باستخدام الإعدادات الافتراضية:

نص العنوان المتحرك. العرض التوضيحي الأدنى: المصدر:

الآن، ينزلق نص العنوان قليلاً إلى الجانب لإفساح المجال لزر الرجوع.


إضافة مؤثرات متحركة إلى عناصر زائفة متعددة بالطريقة نفسها باستخدام view-transition-class

توافق المتصفّح

  • Chrome: 125
  • الحافة: 125.
  • Firefox: غير متوافق
  • Safari Technology Preview: متاح

لنفترض أنّ لديك انتقالًا للعرض يتضمّن مجموعة من البطاقات ولكنّه يتضمّن أيضًا عنوانًا على الصفحة. لتحريك جميع البطاقات باستثناء العنوان، يجب كتابة أداة اختيار تستهدف كل بطاقة على حدة.

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

والنتيجة:

عنصر واحد ينتقل إلى عنصر آخر العرض التوضيحي الأدنى: المصدر:

تنتقل الصورة المصغّرة الآن إلى الصورة الرئيسية. وعلى الرغم من أنها عناصر مختلفة من الناحية النظرية (وحرفيًا)، إلا أنّ واجهة برمجة تطبيقات الانتقال تتعامل معها باعتبارها عناصر مختلفة لأنّها تشترك في view-transition-name نفسها.

إنّ الرمز البرمجي الفعلي لهذا الانتقال أكثر تعقيدًا قليلاً من المثال السابق، لأنّه يعالج أيضًا عملية الانتقال إلى صفحة الصور المصغّرة. اطّلِع على المصدر للتنفيذ الكامل.


انتقالات مخصَّصة للدخول والخروج

راجِع هذا المثال:

الدخول إلى الشريط الجانبي والخروج منه العرض التوضيحي الأدنى: المصدر:

يشكّل الشريط الجانبي جزءًا من عملية النقل:

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

ولكن على عكس العنوان في المثال السابق، لا يظهر الشريط الجانبي على جميع الصفحات. إذا كانت كلتا الحالتَين تتضمّنان الشريط الجانبي، ستظهر العناصر الزائفة لعملية النقل على النحو التالي:

::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) فقط.

في العرض الترويجي السابق، ينتقل الشريط الجانبي بشكل مختلف حسب ما إذا كان يظهر أو يختفي أو يظهر في كلتا الحالتَين. يظهر الشعار من خلال التمرير من اليمين وتدريجه إلى أن يظهر، ويختفي من خلال التمرير إلى اليمين وتدريجه إلى أن يختفي، ويبقى في مكانه عند ظهوره في كلتا الحالتَين.

لإنشاء انتقالات دخول وخروج محدّدة، يمكنك استخدام الفئة الزائفة :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;
}

في هذه الحالة، لا يوجد انتقال محدد عندما يكون الشريط الجانبي موجودًا في كلتا الحالتين، حيث إن الإعداد الافتراضي مثالي.

تعديلات 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
  • الحافة: 125
  • Firefox: غير متوافق
  • Safari: 18

في بعض الأحيان، يجب أن يكون هناك انتقال مخصّص من عرض معيّن إلى عرض آخر. على سبيل المثال، عند الانتقال إلى الصفحة التالية أو إلى الصفحة السابقة في تسلسل التقسيم على صفحات، قد تحتاج إلى تمرير المحتوى في اتجاه مختلف اعتمادًا على ما إذا كنت ستنتقل إلى صفحة أعلى أو صفحة أقل من التسلسل.

تسجيل العرض التوضيحي لميزة "الفهرسة حسب الصفحات" ويستخدم هذا التأثير انتقالات مختلفة حسب الصفحة التي ستنتقل إليها.

ويمكنك لهذا الغرض استخدام أنواع انتقالات العرض التي تتيح لك تحديد نوع واحد أو أكثر لعملية انتقال عرض نشطة. على سبيل المثال، عند الانتقال إلى صفحة أعلى في تسلسل التقسيم على صفحات، استخدِم النوع forwards وعند الانتقال إلى صفحة أقل استخدِم النوع backwards. ولا تكون هذه الأنواع نشطة إلا عند التقاط انتقال أو تنفيذه، ويمكن تخصيص كل نوع من خلال CSS لاستخدام رسوم متحركة مختلفة.

لاستخدام الأنواع في عملية انتقال لعرض المستند نفسه، عليك تمرير types إلى الطريقة startViewTransition. للسماح بذلك، تقبل document.startViewTransition أيضًا كائنًا: update هي دالة الاستدعاء التي تعدِّل 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، وهو وعد يتمّ حلّه فور وصول عملية النقل إلى حالتها النهائية. يتم تناول الخصائص الأخرى لهذا العنصر في مرجع واجهة برمجة التطبيقات.

يمكنك الآن استخدام اسم الفئة هذا في 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;
}

هذا كل ما في الأمر!

انتقال الفيديو، أبطأ. العرض التوضيحي الأدنى: المصدر:

يتم الآن تشغيل الفيديو أثناء عملية النقل.


التكامل مع واجهة برمجة تطبيقات التنقّل (وأُطر العمل الأخرى)

يتم تحديد انتقالات العرض بطريقة يمكن دمجها مع أُطر عمل أو مكتبات أخرى. على سبيل المثال، إذا كان تطبيقك المكوّن من صفحة واحدة يستخدم جهاز توجيه، يمكنك تعديل آلية تعديل جهاز التوجيه لتعديل المحتوى باستخدام انتقال عرض.

في المقتطف التالي من الرمز البرمجي المأخوذ من هذه النسخة التجريبية من تقسيم الصفحات، تم تعديل معالج اعتراض 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);
            });
        }
    });
});

توفّر بعض المتصفّحات، وليس كلها، انتقالًا خاصًا بها عندما ينفّذ المستخدم إيماءة التمرير السريع للتنقّل. في هذه الحالة، يجب عدم بدء عملية انتقال العرض لأنّ ذلك سيؤدي إلى تجربة مستخدم سيئة أو مربكة. سيرى المستخدم انتقالَين، أحدهما يقدّمه المتصفّح والآخر تقدّمه أنت، وسيتم تنفيذهما بالتتابع.

لذلك، يُنصح بمنع بدء انتقال العرض عندما يقدّم المتصفّح انتقالًا مرئيًا خاصًا به. لتحقيق ذلك، تحقّق من قيمة السمة 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، وهو وعد يتمّ حله بعد إنشاء العناصر الصورية الصورية بنجاح. يتم تناول الخصائص الأخرى لهذا العنصر في مرجع واجهة برمجة التطبيقات.


الانتقالات كميزة تحسين

تم تصميم واجهة برمجة التطبيقات 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 عندما "تستقر" طريقة العرض الجديدة، أي عندما يكتمل أي انتقال متحرك أو يتم تخطّيه إلى النهاية، استبدِل switchView بـ transition.finished.transition.updateCallbackDone


هذه ليست قيمة 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، الذي يطبّق مجموعة من تغييرات الحالة بشكل متزامن. نعم، هناك تحذير كبير بشأن استخدام واجهة برمجة التطبيقات هذه، ولكنّ دان أبراموڤ يؤكّد لي أنّها مناسبة في هذه الحالة. كالمعتاد في React والرمز غير المتزامن، عند استخدام الوعود المختلفة التي يعرضها startViewTransition، يجب التأكّد من أنّ الرمز يعمل بالحالة الصحيحة.
  • Vue.js: المفتاح هنا هو nextTick، الذي يتم تنفيذه بعد تعديل عنصر DOM.
  • Svelte: تشبه Vue كثيرًا، ولكن طريقة انتظار التغيير التالي هي tick.
  • Lit: المفتاح هنا هو وعد this.updateComplete ضمن المكوّنات، والذي يتم تنفيذه بعد تعديل DOM.
  • Angular: المفتاح هنا هو applicationRef.tick، الذي يُزيل التغييرات المعلّقة في نموذج DOM. اعتبارًا من الإصدار 17 من Angular، يمكنك استخدام withViewTransitions المضمّن في @angular/router.

مرجع واجهة برمجة التطبيقات

const viewTransition = document.startViewTransition(update)

ابدأ ViewTransition جديدًا.

update هي دالة يتمّ استدعاؤها بعد تسجيل الحالة الحالية للمستند.

بعد ذلك، عندما يتم استيفاء الوعد الذي تم إرجاعه بواسطة updateCallback، يبدأ الانتقال في الإطار التالي. إذا تم رفض الوعد الذي تم إرجاعه من خلال 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

يتم وضعها بشكل مطلق في أعلى يمين الغلاف.

تملأ 100% من عرض المجموعة، ولكنّ ارتفاعها تلقائي، لذا ستحافظ على نسبة العرض إلى الارتفاع بدلاً من ملء المجموعة.

يجب أن يتضمّن mix-blend-mode: plus-lighter للسماح بتمويه حقيقي.

يتم نقل طريقة العرض القديمة من opacity: 1 إلى opacity: 0. ينتقل العرض الجديد من opacity: 0 إلى opacity: 1.


ملاحظات

نحن نقدِّر دائمًا ملاحظات المطوّرين. لإجراء ذلك، يمكنك الإبلاغ عن مشكلة مع فريق عمل CSS على GitHub وتقديم اقتراحات وأسئلة. أضِف [css-view-transitions] إلى بداية مشكلتك.

إذا واجهت خطأً، يمكنك الإبلاغ عن خطأ في Chromium بدلاً من ذلك.