การเปลี่ยนคอมโพเนนต์ที่กำหนดขอบเขต

การเริ่มการเปลี่ยนมุมมองด้วย document.startViewTransition() จะแนบการวางซ้อน ::view-transition กับองค์ประกอบ :root ซึ่งจะบล็อกเหตุการณ์เคอร์เซอร์ในส่วนที่เหลือของหน้า ป้องกันการเปลี่ยนพร้อมกัน และบดบังส่วนหัว position: fixed หรือองค์ประกอบ [popover] ที่เปิดอยู่ (แม้ว่าจะใช้กลุ่มการเปลี่ยนมุมมองที่ฝังเพื่อป้องกันไม่ให้เนื้อหาล้นออกมาก็ตาม)

การเรียกใช้ element.startViewTransition() ในรูทของคอมโพเนนต์จะช่วยแก้ปัญหานี้ได้โดยกำหนดขอบเขตการเปลี่ยนภาพไปยังซับทรี DOM นั้น โดยระบบจะแทรกต้นไม้ Pseudo-Element ::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 ของตัวเองโดยอัตโนมัติเนื่องจากสไตล์ชีต User Agent ใช้ view-transition-name: root และ view-transition-group: contain กับองค์ประกอบดังกล่าว ด้วยวิธีนี้ เมื่อเรขาคณิตของขอบเขตได้รับผลกระทบจากการเปลี่ยนแปลงที่คุณทำใน DOM เรขาคณิตจะเคลื่อนไหวเรขาคณิตของตัวเองไปพร้อมกับองค์ประกอบย่อยอย่างราบรื่น โดยเป็นส่วนหนึ่งของ View Transition

เมื่อขอบเขตตัดส่วนที่เกินออก (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 ให้กับองค์ประกอบที่รวมขอบเขตด้านใน จากนั้นทรานซิชันด้านนอกจะจับภาพและทำให้เฉพาะ Wrapper เคลื่อนไหวแทนองค์ประกอบขอบเขตด้านใน ซึ่งช่วยให้ทรานซิชันด้านในทำงานจนเสร็จสมบูรณ์ได้

ตัวอย่าง

ไม่จำเป็นต้องใช้ 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();
    }
  });
}

กำลังเรียกใช้การเปลี่ยนภาพด้านนอก

เมื่อเริ่มการเปลี่ยนมุมมองนอกขอบเขตที่ต้องการ (เช่น จากคอมโพเนนต์หลัก หรือจากเอกสารสำหรับการเปลี่ยนระดับหน้าเว็บ) การเปลี่ยนภายนอกจะจับภาพและทำให้องค์ประกอบขอบเขตและทุกอย่างภายในเคลื่อนไหว

เพื่อป้องกันไม่ให้เกิดเหตุการณ์นี้ ให้รวมองค์ประกอบขอบเขตไว้ในองค์ประกอบ Wrapper แยกต่างหากที่มี view-transition-name ของตัวเอง และใช้ view-transition-scope: all กับองค์ประกอบขอบเขตด้านใน ซึ่งจะช่วยให้จับภาพ Wrapper และทำให้เคลื่อนไหวได้โดยการเปลี่ยนภาพด้านนอก ขณะเดียวกันก็ป้องกันไม่ให้จับภาพเนื้อหาของขอบเขตด้านใน

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

หรือจะอนุญาตให้การเปลี่ยนภาพด้านนอกจับภาพองค์ประกอบย่อยก็ได้ หากต้องการคงเอฟเฟกต์การคลิปไว้ ให้ใช้กลุ่มการเปลี่ยนมุมมองที่ฝังเพื่อคลิปองค์ประกอบย่อยที่บันทึกไว้ที่ขอบเขตของ Wrapper อย่างไรก็ตาม ในกรณีนี้ คุณต้องใช้ clip กับ ::view-transition-group-children() เทียมที่ได้ด้วยตนเอง

.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(*)
  • บังคับ: คงโฟกัสของแป้นพิมพ์ไว้เมื่อนำองค์ประกอบที่โฟกัสออกระหว่างการเปลี่ยนผ่านโดยเปลี่ยนโฟกัสไปยังรายการหรือการควบคุมที่อยู่ติดกันภายใน Callback การอัปเดต
  • ควรทำ: ใช้กลุ่มการเปลี่ยนมุมมองที่ฝัง (view-transition-group: contain และ overflow: clip ใน ::view-transition-group-children()) เมื่อองค์ประกอบที่บันทึกภายในขอบเขตตัดส่วนที่เกินของตัวเองออกและมีองค์ประกอบที่บันทึกที่ฝังอยู่
  • ควรทำ: แนบ transition.ready.catch(() => {}) เมื่อการโต้ตอบที่รวดเร็วสามารถทริกเกอร์ element.startViewTransition() ซ้ำในองค์ประกอบขอบเขตเดียวกัน
  • สิ่งที่ควรทำ: รวมองค์ประกอบขอบเขตภายในไว้ในองค์ประกอบ Wrapper ที่มีชื่อแยกต่างหาก หากการเปลี่ยนภาพภายนอกจำเป็นต้องทำให้ตำแหน่งของคอมโพเนนต์เคลื่อนไหวขณะที่การเปลี่ยนภาพภายในกำลังทำงานอยู่
  • อย่า: ตั้งค่า view-transition-name: none ในองค์ประกอบขอบเขตที่ตัดส่วนที่เกินออก (overflow: hidden, scroll หรือ clip) หรือเปลี่ยนเรขาคณิตของตัวเอง เนื่องจากการเลือกไม่ใช้การมีส่วนร่วมด้วยตนเองจะปิดใช้การตัดส่วนที่เกินออกโดยอัตโนมัติ

กลยุทธ์สำรอง

หาก Baseline target ไม่รองรับการเปลี่ยนมุมมองที่กำหนดขอบเขตองค์ประกอบ ให้ลดระดับลงอย่างราบรื่นโดยเรียกใช้ Callback การอัปเดต 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() เป็นทางเลือกสำรอง คุณจะต้องตรวจสอบว่าเอฟเฟกต์การตัดคลิปของคอนเทนเนอร์ยังคงอยู่ ตามที่อธิบายไว้ในส่วนการเรียกใช้ทรานซิชันภายนอก โปรดทราบว่าแนวทางสำรองนี้จะทำให้ความสามารถในการเรียกใช้ View Transition หลายรายการพร้อมกันใช้งานไม่ได้ และจะบดบังองค์ประกอบ position: fixed และ [popover] ในเลเยอร์บนสุด