view-transitions-contracts

Installation
SKILL.md

View transitions contracts

A view transition is a visual enhancement wrapped around a DOM change: the DOM update always happens, so a broken transition fails silently — no console error, just no animation (or a frozen frame). This lens spots the silent-abort, stale-snapshot, and reduced-motion bugs in review; it is not a guide for building transitions.

Checklist (lead with the trap)

  1. Duplicate view-transition-name on the same frame silently aborts the whole transition. If two rendered elements carry the same name at the same time, ViewTransition.ready rejects and the animation is skipped — but the DOM still updates, so it reads as "the animation randomly doesn't fire." This is the classic list bug: reusing one static name across items. Suffix each with a unique id/key (or use view-transition-name: match-element where supported).
  2. A frozen or stale old snapshot means the incoming DOM was not painted when the snapshot was taken. Snapshots are paint-based images, not live DOM. If the new view is still a Suspense fallback, a not-yet-streamed server component, or an image that has not decoded, the new snapshot captures the wrong/empty state and the old frame appears stuck. Hoist the matched (shared) element above the Suspense boundary so it exists on both sides; await image decode() / data readiness before startViewTransition. React <ViewTransition> waits for stylesheets and up to ~500ms for fonts, but not arbitrary data.
  3. prefers-reduced-motion is NOT honored automatically. The UA default cross-fade plus the group transform still run. Needs an explicit reduced-motion block on the pseudos, e.g. @media (prefers-reduced-motion: reduce) { ::view-transition-group(*), ::view-transition-old(*), ::view-transition-new(*) { animation: none !important; } } (or gate startViewTransition in JS / pass skipTransition). Note reduced does not mean none — animation: none is an instant cut; a very short cross-fade is often the gentler choice. Missing this is an accessibility defect, not a nicety.
  4. React: the state update must be inside a Transition, and flushSync opts you out. With <ViewTransition>, the mutation must run inside startTransition — plain setState, useSyncExternalStore, and updates after an await/setTimeout are not marked as Transitions and will not animate; a stray flushSync mid-flow makes React skip the transition entirely. The vanilla API is the opposite: you wrap the setState in flushSync inside the startViewTransition callback so the DOM applies synchronously before the snapshot. Pick one pattern; do not mix them.
  5. Only one transition runs at a time; a new one interrupts and skips the current. skipTransition() and interruption cancel only the animation — updateCallback and the DOM change still run. "It skipped" never means "the state did not update." Rapid navigations fast-forward to the end state (a visual jump); chain update callbacks into one startViewTransition if smoothness matters.
  6. Leftover view-transition-name causes ghost animations. A dynamically-set name not cleared after the snapshot persists (including in the bfcache on back/forward), so an unrelated element morphs later, or a duplicate-name abort appears on the next pagereveal. Remove names once the snapshot has been taken.
  7. Fixed chrome and top-layer content can paint behind the overlay. A document-scoped transition paints the ::view-transition layer above everything (including the top layer), so position: fixed headers and popovers get baked into the flat root snapshot and slide/disappear. Give them their own view-transition-name plus a high ::view-transition-group() z-index, or use an element-scoped transition.
  8. iframe / cross-origin content cannot participate. Transitions are same-origin only; cross-document transitions are main-frame + same-origin with a matching @view-transition opt-in, and iframe content is not snapshotted (it reloads when moved). Do not promise a shared-element morph across an iframe or cross-origin boundary.

Quick probes

Use these as leads, then read the transition-to-snapshot path:

Installs
1
GitHub Stars
1
First Seen
3 days ago
view-transitions-contracts — voidmatcha/frontend-niche-skills