Prompt and context
A product wants coherent transitions between list, detail, and filtered views without sacrificing native navigation, accessibility, or low-end performance. Design a progressive enhancement plan: use document.startViewTransition() for same-document updates, CSS declarations for cross-document navigation, and explain behavior when the API is unavailable, a DOM update fails, or the user prefers reduced motion.
This fits frontend, design-system, and performance roles. MDN, Chrome for Developers, and the CSS View Transitions specification define the ViewTransition lifecycle, same-document and cross-document mechanisms, pseudo-element animation tree, and skip behavior. This article is derived from public standards, not a claim about a company’s interview bank.
What the interviewer evaluates
The interviewer is testing whether you separate navigation semantics, DOM updates, animation lifecycle, and fallback boundaries. A strong answer mentions ready, updateCallbackDone, finished, skipTransition(), prefers-reduced-motion, unique view-transition-name values, and same-origin limits. A weak answer adds a fade-in CSS rule without a failure plan.
Clarifying questions
- Is the target an SPA state change, cross-document navigation, or both?
- Which elements need shared identity, and do list cards have stable IDs when opening details?
- Must core navigation and indexing work with no JavaScript?
- What animation duration, low-end device budget, and reduced-motion requirement does the product have?
A 30-second answer
“I would keep native navigation and state updates as the source of truth and use View Transitions as progressive enhancement. For an SPA, wrap a synchronous or asynchronous DOM update in startViewTransition(update) and animate pseudo-elements after ready; for multi-page navigation, use same-origin @view-transition { navigation: auto; } with stable names for shared elements. If the API is missing, the update fails, or reduced motion is requested, complete the update without animation and call skipTransition() when needed. Unique names, timeouts, and metrics prevent transitions from blocking product behavior.”
Step-by-step solution
For a same-document transition, call document.startViewTransition(() => update()). The browser captures the old state, runs the update callback, and creates the new state; the returned ViewTransition exposes ready, updateCallbackDone, and finished Promises. If the callback throws, the application still owns error handling; business submission must not depend on animation success. Customize the transition with pseudo-elements such as ::view-transition-old(root) and ::view-transition-new(root).
Use view-transition-name to pair an old element with its new counterpart. Names must be unique in one transition tree. Lists need stable business IDs rather than array indexes, otherwise sorting or filtering pairs the wrong elements. Name only important elements; too many snapshots and compositor surfaces increase cost.
Cross-document transitions rely on same-origin navigation and the CSS @view-transition declaration; JavaScript does not call startViewTransition() for the navigation. This works for multi-page applications but depends on browser support, same-origin policy, and page CSS. A link must still navigate normally without a transition; an animation Promise must not block the server response or default link behavior.
Lifecycle handling needs timeout and skip paths. If ready does not settle within a budget, record the reason and call skipTransition(). Back navigation, form submission, and leaving the page should prioritize navigation. Use finished only for cleanup and metrics, never as proof that data was saved. For asynchronous data, define the state-commit boundary before deciding whether to await the transition.
Accessibility means honoring @media (prefers-reduced-motion: reduce) by setting animation duration to zero or skipping it. Preserve focus, headings, and reading order; visual position cannot replace semantic updates. Keyboard users, screen readers, and slow devices must receive the same content result.
Progressive enhancement combines capability detection with real navigation fallback. If document.startViewTransition is absent, call the update function directly; if cross-document support is absent, keep ordinary links. Measure support rate, skip rate, ready latency, frame rate, and interaction latency by browser and device. A transition may change presentation, but never cache, permission, form-submission, or routing correctness.
Model answer
I would keep navigation and state submission independent of animation. An SPA uses startViewTransition(update) and animates only essential shared elements after ready; a multi-page app uses same-origin @view-transition. Each shared element gets a stable, unique business name, and a failed asynchronous update skips the transition while showing the normal error state.
Unsupported browsers use native updates or links. Reduced-motion users get no animation or a shortened one. Lifecycle timeouts retain skipTransition(). Focus, headings, and semantic order never depend on the visual effect. Monitor support, skips, and interaction latency, then expand the named-element set after a canary.
Common mistakes
- Mistake → use an array index as
view-transition-name; Why it fails → sorting pairs the wrong old and new nodes; Fix → use a stable business ID and enforce uniqueness. - Mistake → wait for
finishedbefore submitting a form; Why it fails → an animation failure blocks the business action; Fix → commit state first and animate only presentation. - Mistake → design only the SPA path; Why it fails → multi-page navigation has no JavaScript call site; Fix → use same-origin
@view-transitionand preserve ordinary links. - Mistake → ignore reduced-motion preferences; Why it fails → sensitive users may experience dizziness or cognitive load; Fix → shorten or skip the animation in the media query.
Follow-up questions
How should a failed update callback affect the transition and state?
Treat the update callback as the business-state boundary: catch the exception, roll back or render an error, and keep the transition Promise limited to animation lifecycle. If updateCallbackDone rejects, call skipTransition() or let the browser finish the default presentation; do not swallow the error.
Why not name every element as a shared element?
Each name must form an unambiguous pair between old and new trees. Naming everything increases capture, layout, and compositing cost and can create accidental matches. Name only key objects that users understand; use a root fade or no animation for the rest.
How do you prove the transition does not hurt performance?
Measure ready latency, frame rate, long tasks, first-input latency, and skip rate by browser and device, then test realistic list sizes. If low-end devices show sustained frame loss, reduce the animation scope or skip it automatically while keeping navigation time unchanged.