Prompt and scope
A product wants View Transition API effects for SPA route changes and opening an image detail view. Explain the implementation and how you handle older browsers, prefers-reduced-motion, async data, focus, animation failure, and performance fallback.
This tests API boundaries and progressive enhancement, not a demo animation. MDN defines document.startViewTransition() as the same-document update entry point and says the transition proceeds after the callback completes; cross-document transitions also require same origin and CSS @view-transition. Animation must serve state changes and never block usability.
What the interviewer is testing
First, can you distinguish same-document SPA, element-scoped, and cross-document transitions? Second, can you handle unsupported APIs, rejected callbacks, reduced motion, and page exit? Third, can you preserve focus, semantic DOM, scrolling, and real interaction instead of hiding them behind a snapshot animation?
Questions to clarify before answering
- What boundary is transitioning? A same-document DOM update, one element, or navigation between documents?
- Does the update include async data? What must be ready first, and what is the maximum wait?
- What is the acceptable older-browser experience? The same state update must work without animation.
- Which users reduce or disable motion? How do
prefers-reduced-motionand an app setting combine? - How is page state restored? Focus, scroll, form input, and back navigation?
- What is the performance budget? Snapshot scope, duration, low-end devices, and concurrent transitions?
A 30-second answer framework
“I would separate state update from animation: when supported, wrap a synchronous DOM update in startViewTransition; otherwise run the same update directly. Prepare async data in the route or component, keep the callback focused on committing ready state, and observe ready, finished, and skipTransition. Use named views to limit scope, shorten or disable motion under prefers-reduced-motion, restore focus and scroll, and ensure animation failure cannot block interaction. Validate long tasks, snapshot count, completion time, and fallback rate on low-end devices.”
Step-by-step deep dive
Step 1: Define the state update and animation goal
Name the two states the animation connects, such as a list card to a detail page or filtered results to a new list, instead of fading the whole page indiscriminately. The update must be correct without animation; animation is an enhancement layer. Define allowed transition names and fallback behavior for each navigation type.
Step 2: Detect support and keep a synchronous fallback
Check document.startViewTransition before calling it. Unsupported browsers execute the same update function directly; do not duplicate business logic. New-browser support does not cover every older device, so keep a fallback path as MDN’s compatibility guidance requires.
Step 3: Bound the callback and async data
updateCallback runs after the old-view snapshot; its returned Promise must fulfill before the next frame. Fetch data before starting where possible. If the callback must wait, set a timeout and allow the transition to be skipped. A rejected callback follows normal business error handling and must not expose a half-updated page.
const update = () => renderState(nextState);
if (!document.startViewTransition || reduceMotion) {
update();
} else {
const transition = document.startViewTransition(update);
transition.finished.catch(() => {
// Animation failure does not roll back the completed state update.
});
}Step 4: Limit snapshots with named views
Give a unique view-transition-name only to elements that truly share a position. Repeated list components cannot share one name without ambiguity. For dynamic lists, clear old names before updating and assign stable unique names afterward.
Step 5: Implement reduced motion and accessible behavior
Listen for prefers-reduced-motion: reduce, set duration to zero or near zero, and keep the state change. Do not hide keyboard focus or communicate results only through color. After the update, move focus to the new view heading or equivalent. web.dev warns that full-screen view-transition animations can discomfort people with vestibular disorders.
Step 6: Handle SPA, element, and cross-document differences
An SPA wraps its DOM update with document.startViewTransition; an element-scoped transition affects the calling element and descendants; a cross-document navigation requires same origin and @view-transition opt-in in both documents. Do not assume a same-document JS callback exists across the document lifecycle.
Step 7: Design skip, concurrency, and failure paths
For rapid clicks, cancel or coalesce stale navigation so transitions do not compete for the same node. Use skipTransition() or a timeout to end a stuck animation. Once business state is updated, a visual failure must not cause duplicate submissions, duplicate requests, or an incorrect rollback.
Step 8: Validate performance and real user experience
Monitor start-to-finish time, skip ratio, long tasks, CLS, input delay, and low-end-device errors. Exercise large lists, slow networks, rejected async work, rapid back navigation, screen readers, and reduced motion. Confirm snapshots and animation never make real interaction wait longer.
Trade-offs and boundaries
Trade-off 1: Full-page or local transition
Full-page transitions are simpler but cost more snapshots and can interfere with focus and scroll. Local transitions require stable names and precise component boundaries, making them better for frequent interactions and large pages.
Trade-off 2: Wait for data or transition immediately
Waiting avoids old/new content mismatch but increases response time. Prefer prefetching; otherwise show explicit loading state. A transition is not a replacement for loading feedback.
Trade-off 3: Custom or browser-default animation
Defaults are safer and easier to maintain. Override pseudo-element animation only with a clear navigation semantic and verification budget, and provide an equally functional static state for reduced motion.
Failure drills and evolution plan
Drill 1: Unsupported browser or rejected callback
Update directly in an unsupported browser, reject the Promise, and verify the page state remains correct. Log only diagnostic information and never block continued interaction.
Drill 2: Rapid navigation and async race
Click two links rapidly while making the first request slower. Verify the stale transition cannot overwrite current state, focus lands on the current page, and no duplicate side-effect request is sent.
Drill 3: Reduced motion and low-end device
Enable system reduce motion and run a large-list transition on a CPU-constrained device. Confirm animation is disabled or shortened while input delay and layout stability stay within budget.
Common mistakes and follow-ups
Mistake 1: No fallback path
Unsupported APIs must still produce the same DOM or route update. Progressive enhancement cannot bind business state to animation success.
Mistake 2: Waiting forever for network inside the callback
Long waits after the snapshot freeze the old page. Prepare data earlier or set a timeout and skip the animation.
Mistake 3: Giving many elements one shared name
Duplicate names create matching ambiguity and extra snapshots. Name only the element that truly moves and keep its name unique and stable.
Mistake 4: Ignoring focus and scroll
A finished visual transition does not return keyboard users to the right place. Explicitly restore focus, scroll, and the semantic heading.
Mistake 5: Treating reduced motion as removing the feature
Users ask for less motion, not less state or information. Keep the same interaction and change only the animation presentation.
Mistake 6: Rolling back business state when animation fails
finished rejection usually describes a visual-stage failure. Do not resubmit or roll back a state update that already succeeded.