How Google created a connected experience for its new AI Mode using view transitions

Published: Aug 28, 2025

Google Search has one of the largest reaches in the world, therefore changes to our user experience can have an impact on billions of users. We've long dreamed of a web experience that feels more modern and app-like. When development began on AI Mode, we wanted to create an experience for our users where the transition into AI Mode from standard search felt seamless and connected. When we heard about cross-document view transitions we knew it was a perfect pairing for the feature. This case study shares what we learned adding the transition feature alongside the launch of AI Mode.

Recording of a search with Google Search, switching from the search results into AI Mode. The transition is using view transitions.

Cross-document view transitions are a game changer when it comes to native browser tooling and we're excited to see how it will shape the web going forward.

Browser Support

  • Chrome: 126.
  • Edge: 126.
  • Firefox: not supported.
  • Safari: 18.2.

Source

Changing the status quo

Google Search has strict and conservative browser support requirements. Generally, using a limited availability feature has been off limits. For cross-document view transitions, we found that a polyfill was not tenable, with the main blocker being that there was no pixel snapshotting API and cloning the entire viewport had major performance issues. Therefore, using the feature as a progressive enhancement was the best way to launch alongside AI Mode. Since the animations created by view transitions do not directly impact the functionality of the website, for unsupported traffic they would simply be disabled, which was already the current production state without transition animations.

We first tested this progressive enhancement strategy with our internal users. This provided us with early feedback, which was overwhelmingly positive. The feedback received also surfaced bugs, including performance issues and unintended interactions with other features such as overlapping stacking contexts.

We found this strategy to be successful and I believe that we will try it with other new browser features going forward.

Difficulties we encountered, and solving them

Latency, render blocking, and watchdog timers

Overall, the added latency that comes with MPA view transitions is negligible for 99% of use-cases, especially on modern hardware. However, Google Search has an extremely high bar when it comes to latency and we strive to create user experiences that work well on all devices. For us, even a few extra milliseconds matter, so we had to invest into how to properly implement cross-document view transitions without harming the user experience for anyone.

Render blocking is a technique that pairs well with cross-document view transitions. The pseudo-element snapshots of the incoming document can only display content that has already been rendered. Therefore, to animate content from the incoming document, you need to render block until the target element you want to animate has been rendered. To do this use the blocking attribute on an HTMLLinkElement. Render blocking has its drawbacks as waiting for an element that is towards the end of the incoming document's DOM tree may have a substantial latency impact. We had to balance this tradeoff accordingly and only render block on elements that render extremely early in the page lifecycle.

<!-- Link tag in the <head> of the incoming document -->
<link blocking="render" href="#target-id" rel="expect">
<!-- Element you want to animate in the <body> of the incoming document -->
<div id="target-id">
  some content
</div>

In some cases, being precise about which element you render block on was not sufficient. Certain devices or connections would still see added latency even when render blocking on an element near the beginning of the DOM tree. To handle those cases we wrote a watchdog timer script to remove the HTMLLinkElement after a certain amount of time has elapsed to force unblock rendering of the incoming document.

A simple way to do this is as follows:

function unblockRendering() {
  const renderBlockingElements = document.querySelectorAll(
    'link[blocking=render]',
  );
  for (const element of renderBlockingElements) {
    element.remove();
  }
}

const timeToUnblockRendering = t - performance.now();

if (timeToUnblockRendering > 0) {
  setTimeout(unblockRendering, timeToUnblockRendering);
} else {
  unblockRendering();
}

Coverage limitations

Another issue we encountered is that the cross-document view transitions at-rule navigation: auto happens at a global level within the document. There's no built-in way to scope the enabling of cross-document view transitions to only specific click targets. Since this is such a large change, we couldn't enable cross-document view transitions on 100% of navigations on Google Search. We needed a way to dynamically enable or disable cross-document view transitions depending on what feature the user was interacting with. In our case, we only enabled them for mode changes to and from AI Mode. We did so by programmatically updating the navigation at-rule depending on which target was clicked or tapped.

A way to toggle the view transition at-rule is as follows:

let viewTransitionAtRule: HTMLElement | undefined;
const DISABLED_VIEW_TRANSITION = '@view-transition{navigation:none;}';
const ENABLED_VIEW_TRANSITION = '@view-transition{navigation:auto;}';

function getVtAtRule(): HTMLElement {
  if (!viewTransitionAtRule) {
    viewTransitionAtRule = document.createElement('style');
    document.head.append(viewTransitionAtRule);
  }
  return viewTransitionAtRule;
}

function disableVt() {
  getVtAtRule().textContent = DISABLED_VIEW_TRANSITION;
}

function enableVt() {
  getVtAtRule().textContent = ENABLED_VIEW_TRANSITION;
}

Jank and composited animations

Some of the automatically generated animations on the view transition pseudo-elements caused frame drops on older devices, damaging the clean, seamless experience we want to offer users. To improve the performance of the animations, we rewrote them using animation techniques that can run on the compositor. We were able to do so by inspecting the keyframes to get the dimensions of the before and after snapshot pseudo-elements and using matrix math to rewrite the keyframes accordingly. The following example shows how to grab the animation for each view transition pseudo-element:

const pseudoElement = `::view-transition-group(${name})`;
const animation = document
  .getAnimations()
  .find(
    (animation) =>
      (animation.effect as KeyframeEffect)?.pseudoElement === pseudoElement,
  );

Read more about writing performant view transition keyframes in View Transitions Applied: Dealing with the Snapshot Containing Block.

Other things to watch out for

One of the more prominent issues is that tagging elements with the view-transition-name CSS property impacts the stacking context (View transitions specification: Section 2.1.1). This was the source for multiple bugs which required the z-index of container elements to be modified.

Another thing to be aware of is that you might not want to add view-transition-name values to elements by default. There are a lot of people working on Google Search. To prevent the view-transition-name values our team sets on elements conflicting with values people from other teams might use, we made use of view transition types to conditionally add the view-transition-name property only while a specific view transition type is active.

Example CSS to add the view-transition-name of the-element to an element only when the view transition type of ai-mode is active:

html:active-view-transition-type(ai-mode) {
  #target {
    view-transition-name: the-element;
  }
}

Once you have these CSS rules in place for all of your view transitions, you can then dynamically change the current view transition type for any navigation during the pageswap and pagereveal events.

Example of updating the view transition type to ai-mode during the pageswap event.

function updateViewTransitionTypes(
  event: ViewTransitionEvent,
  types: string[],
): void {
  event.viewTransition.types.clear();
  for (const type of types) {
    event.viewTransition.types.add(type);
  }
}

window.addEventListener(
  'pageswap',
  (e) => {
    updateViewTransitionTypes(
      e as ViewTransitionEvent,
      ['ai-mode'],
    );
  }
);

This way we prevent naming collisions and don't unnecessarily snapshot elements that don't need to be snapshotted as part of going to and from AI Mode.

Lastly, any stacking context issues would only be present during the view transition. To resolve those issues, we can then target the z-indices of the generated pseudo elements, instead of arbitrarily modifying z-indices of the original elements for the sole reason to resolve this issue when using view transitions.

::view-transition-group(the-element) {
  z-index: 100;
}

What's next

We have plans to use cross-document view transitions for Google Search, including integration with the Navigation API once it becomes available cross-browser. Stay tuned to see what we end up building next!