يؤدي بدء انتقال العرض باستخدام document.startViewTransition() إلى إرفاق تراكب ::view-transition بالعنصر :root، ما يؤدي إلى حظر أحداث المؤشر على بقية الصفحة، ومنع الانتقالات المتزامنة، وحجب عناوين position: fixed أو فتح عناصر [popover] (حتى عند استخدام مجموعات انتقال العرض المُدمجة لمنع تسرُّب المحتوى الزائد).
يؤدي استدعاء element.startViewTransition() على جذر مكوّن إلى حلّ هذه المشكلة من خلال تحديد نطاق الانتقال إلى تلك الشجرة الفرعية لنموذج DOM: يتم إدخال شجرة العناصر الزائفة ::view-transition مباشرةً في العنصر، ما يحافظ على سياقات التجميع الخارجية وتفاعلية الصفحة مع السماح لمكوّنات متعددة بالانتقال في الوقت نفسه.
التنفيذ
1. تفعيل عملية انتقال محدودة النطاق
يمكنك استدعاء element.startViewTransition() في عنصر الحاوية الجذر للمكوّن بدلاً من document.startViewTransition(). يصبح العنصر الذي تستدعي startViewTransition() عليه نطاق الانتقال (عنصر النطاق): يتم تسجيل العناصر ضمن شجرته الفرعية فقط، ويتوقف عرض النطاق فقط أثناء معاودة الاتصال بالتعديل، ويتطابق تراكب ::view-transition الناتج مع حجم عنصر النطاق وشكله وسياق التكديس.
2. تسمية العناصر المشارِكة
يمكنك تعيين view-transition-name للعناصر الثانوية داخل النطاق التي يجب أن تكون متحركة. يمكنك استخدام view-transition-name: match-element في عناصر القائمة الديناميكية لاستخلاص اسم انتقال كل عنصر تلقائيًا من هوية عقدة DOM الخاصة به، أو تخصيص أسماء صريحة (يجب أن تكون فريدة ضمن هذا النطاق فقط).
يتم عزل الأسماء داخل كل نطاق، ما يمنع حدوث تداخلات بين المكوّنات في أجزاء مختلفة من DOM. ومع ذلك، يمكن أن تتعارض view-transition-name مع الأسماء في انتقالات العرض المُفصَّل المُدمَجة. لمنع حدوث ذلك، أضِف view-transition-scope: all إلى عنصر النطاق الفرعي.
3- المشاركة الذاتية واقتصاص الفائض
يشارك عنصر النطاق تلقائيًا في انتقال العرض الخاص به لأنّ ورقة أنماط وكيل المستخدم تطبّق view-transition-name: root وview-transition-group: contain عليه. بهذه الطريقة، عندما يتأثر شكل النطاق بالتغيير الذي تجريه في DOM، يتم تحريك شكله بسلاسة مع العناصر الفرعية، كجزء من "انتقال العرض".
عندما يقتطع النطاق المحتوى الزائد (overflow: hidden أو scroll أو clip)، يطبّق ::view-transition-group-children(root) تلقائيًا overflow: clip، ما يمنع العناصر الثانوية التي يتم نقلها من الخروج عن حدود المكوّن.
4. الانتقالات المتزامنة والمتداخلة
يمكن تشغيل أشجار فرعية منفصلة للمكوّنات في element.startViewTransition() في الوقت نفسه بدون تخطّي بعضها البعض. تتلقّى النطاقات النشطة تلقائيًا view-transition-scope: all في ورقة أنماط UA كي لا تتسرّب قيم view-transition-name الداخلية إلى الانتقالات الخارجية. طبِّق السمة view-transition-scope: all في CSS على حاويات المكوّنات المتكررة لمنع الانتقالات الخارجية من التقاط الأسماء الداخلية حتى عندما يكون المكوّن غير نشط.
إذا كانت عملية الانتقال الخارجية بحاجة إلى نقل مكوّن أثناء نشاط عملية الانتقال الداخلية، يمكنك تعيين view-transition-name لعنصر يلتف حول النطاق الداخلي. بعد ذلك، تلتقط الحركة الانتقالية الخارجية الحاوية فقط وتُحرّكها بدلاً من عنصر النطاق الداخلي، ما يسمح بإكمال الحركة الانتقالية الداخلية.
مثال
لا حاجة إلى استخدام CSS في عنصر النطاق نفسه (.card أو .card ul) لإنشاء النطاق، بل يتم تحديد النطاق من خلال استدعاء startViewTransition() في العنصر بدلاً من document (إذا كان المكوّن سيتم دمجه داخل انتقال عرض خارجي، يُرجى الاطّلاع على تشغيل انتقال خارجي لـ view-transition-scope: all).
/* 1. Name participating child items */
.card ul li {
view-transition-name: match-element;
}
/* 2. OPTIONAL: Customize transition timing (300ms is an example duration) */
::view-transition-group(*) {
animation-duration: 300ms;
animation-timing-function: ease-in-out;
}
/* 3. MANDATORY: Respect user preference for reduced motion */
@media (prefers-reduced-motion: reduce) {
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
animation: none !important;
}
}
// 1. Trigger a transition scoped to a single component subtree
function runScopedTransition(scopeEl, updateDOM) {
// MANDATORY: Call startViewTransition on the component element, not document
const transition = scopeEl.startViewTransition(() => {
updateDOM();
});
// Re-triggering startViewTransition() on the same element skips the active transition
// and rejects `ready`. Catch the rejection to avoid unhandled promise rejection errors.
transition.ready.catch(() => {});
}
// 2. Reorder items by moving existing DOM nodes (required for `match-element`)
// Scope to `listEl` since only the list order changes, keeping controls outside `listEl` interactive
function reorderItems(listEl, orderedIds) {
const existing = new Map(
Array.from(listEl.children, (li) => [li.dataset.id, li]),
);
runScopedTransition(listEl, () => {
for (const id of orderedIds) {
const li = existing.get(id);
if (li) listEl.appendChild(li);
}
});
}
// 3. Remove an item while preserving exit animation and keyboard focus
// Scope to the outer `cardEl` so the card itself also transitions its height change
function removeItem(cardEl, itemEl) {
const hadFocus = itemEl.contains(document.activeElement);
const nextFocusTarget =
itemEl.nextElementSibling?.querySelector('button') ??
itemEl.previousElementSibling?.querySelector('button') ??
cardEl.querySelector('button');
runScopedTransition(cardEl, () => {
itemEl.remove();
if (hadFocus) {
nextFocusTarget?.focus();
}
});
}
جارٍ تشغيل انتقال خارجي
عند بدء انتقال عرض خارج النطاق المقصود (على سبيل المثال، من مكوّن رئيسي أو من المستند لانتقال على مستوى الصفحة)، سيلتقط الانتقال الخارجي عنصر النطاق وكل ما بداخله ويحرّكه.
لمنع حدوث ذلك، يمكنك تضمين عنصر النطاق في عنصر تضمين منفصل له سمة view-transition-name خاصة به، وتطبيق view-transition-scope: all على عنصر النطاق الداخلي. يتيح ذلك التقاط الغلاف نفسه وتحريكه من خلال الانتقال الخارجي مع منع التقاط محتوى النطاق الداخلي.
<div class="page" id="page">
<div class="card-wrapper">
<section class="card" id="left">
<h2>Card A</h2>
<ul id="left-list">
<li>…</li>
</ul>
</section>
</div>
<div class="card-wrapper">
<section class="card" id="right">
<h2>Card B</h2>
<ul id="right-list">
<li>…</li>
</ul>
</section>
</div>
</div>
document.querySelector('#page').startViewTransition(() => {
// do your DOM updates here...
});
/* Apply a view-transition-name on the wrapper */
.card-wrapper {
view-transition-name: match-element;
}
/* Prevent the contents of the card being captured by an outer transition */
.card {
view-transition-scope: all;
}
.card ul li {
view-transition-name: match-element;
}
بدلاً من ذلك، يمكنك السماح للحركة الانتقالية الخارجية بالتقاط العناصر الفرعية. للاحتفاظ بتأثيرات الاقتصاص، يمكنك استخدام "مجموعات انتقالات العرض المدمجة" لاقتصاص العناصر الفرعية التي تم التقاطها عند حدود برنامج تضمينها. In that case, though, you need to manually apply the clip onto the resulting ::view-transition-group-children() pseudo.
.card-wrapper {
overflow: clip;
view-transition-name: match-element; /* Capture me as part of the outer VT */
view-transition-group: contain; /* Nest children that also have a view-transition-name */
view-transition-class: card; /* For targeting purposes */
}
/* Manually copy back the clip onto the ::view-transition-group-children pseudo */
::view-transition-group-children(.card) {
overflow: clip;
}
.card ul li {
view-transition-name: match-element;
}
القيود وتسهيل الاستخدام
- إجراء إلزامي: يجب استدعاء
startViewTransition()على عنصر الجذر للمكوّن بدلاً منdocumentلكي تبقى عناصرposition: fixedالخارجية وتراكيب[popover]في طبقات في الأعلى ويظل باقي الصفحة تفاعليًا. - إلزامي: يجب الحفاظ على عُقد DOM الحالية ونقلها (بدلاً من إعادة إنشائها باستخدام
innerHTML) عند استخدامview-transition-name: match-element، لأنّmatch-elementيتتبّع هوية عقدة DOM. - إلزامي: يجب احترام تفضيلات المستخدمين في ما يتعلّق بتقليل الحركة باستخدام
@media (prefers-reduced-motion: reduce)من خلال ضبطanimation: none !importantعلى::view-transition-group(*)و::view-transition-old(*)و::view-transition-new(*). - إلزامي: يجب الحفاظ على تركيز لوحة المفاتيح عند إزالة عنصر مركَّز عليه أثناء عملية انتقال من خلال نقل التركيز إلى عنصر أو عنصر تحكّم مجاور داخل دالة الاستدعاء الخاصة بالتعديل.
- يجب: استخدام مجموعات "انتقالات العرض" المتداخلة (
view-transition-group: containوoverflow: clipفي::view-transition-group-children()) عندما يقتطع عنصر تم التقاطه داخل النطاق المحتوى الزائد الخاص به ويحتوي على عناصر متداخلة تم التقاطها. - ما يجب فعله: يجب إرفاق
transition.ready.catch(() => {})عندما تؤدي التفاعلات السريعة إلى إعادة تشغيلelement.startViewTransition()على عنصر النطاق نفسه. - ما يجب فعله: يمكنك تضمين عنصر نطاق داخلي في عنصر تضمين منفصل يحمل اسمًا إذا كانت عملية الانتقال الخارجية تتطلّب تحريك موضع المكوّن أثناء تشغيل عملية انتقال داخلية.
- لا تضبط السمة
view-transition-name: noneعلى عنصر نطاق يقتصّ المحتوى الزائد (overflow: hiddenأوscrollأوclip) أو يغيّر شكله الهندسي، لأنّ إيقاف المشاركة الذاتية يؤدي إلى إيقاف اقتصاص المحتوى الزائد تلقائيًا.
استراتيجيات احتياطية
إذا كان هدفك الأساسي لا يتيح الانتقالات في العرض على مستوى العنصر، يمكنك تقليل الأداء بسلاسة من خلال تشغيل دالة الاستدعاء لتعديل DOM على الفور بدون تأثيرات متحركة. على الرغم من أنّه يمكنك استخدام document.startViewTransition()، لا يُنصح بذلك لتحديثات المكوّنات المعزولة، لأنّ عملية الانتقال على مستوى المستند تحظر عمليات الانتقال المتزامنة للمكوّنات وتحجب عناصر position: fixed و[popover] في الطبقة العليا.
// Feature detect specifically on Element.prototype (not document.startViewTransition)
const supportsScopedTransition = 'startViewTransition' in Element.prototype;
function runWithTransition(scopeEl, updateDOM) {
if (!supportsScopedTransition) {
// Fallback: apply the DOM update immediately without animation
updateDOM();
return;
}
const transition = scopeEl.startViewTransition(() => {
updateDOM();
});
transition.ready.catch(() => {});
}
إذا كنت بحاجة فعلاً إلى استخدام document.startViewTransition() كحلّ احتياطي، عليك التأكّد من الحفاظ على تأثيرات الاقتصاص للحاويات، كما هو موضّح في قسم تنفيذ عملية انتقال خارجية. يُرجى العِلم أنّ هذا النهج الاحتياطي يوقف إمكانية تشغيل انتقالات متعدّدة للعرض في الوقت نفسه، وسيخفي العنصرَين position: fixed و[popover] في الطبقة العليا.