DEV Community

Cover image for The Ghost in the View Transitions API: A Bizarre Debugging Saga with Google AI πŸ‘»
Omar Afifi
Omar Afifi Subscriber

Posted on

The Ghost in the View Transitions API: A Bizarre Debugging Saga with Google AI πŸ‘»

Summer Bug Smash: Clear the Lineup πŸ›πŸ›Ή

This is a submission for DEV's Summer Bug Smash: Clear the Lineup powered by Sentry.

🎨 Project Overview

While working on a major upgrade for my personal portfolio (Check it out here: Live Portfolio Link | GitHub Repo), I built a feature called the Theme Showcase. When a user first loads the homepage, a slick View Transitions API animation sweeps across the screen, temporarily switching the site to one of my custom showcase themes (like "Obsidian" or "Midnight"), before gracefully sweeping back to their chosen default theme. It was a 1.7-second "Wow Effect" designed to show off the site's dynamic color system.

πŸ› Bug Fix

Soon after deploying, I noticed a terrifying, chaotic bug. Sometimes, I would open the website and quickly switch to another tab to check an email, or minimize the browser window completely. When I switched back...

Instead of my beautiful default theme, the website was permanently stuck on one of the other showcase themes! The animation had frozen mid-way, leaving the site in an awkward intermediate state. While the website was still fully functional and I could manually fix it by selecting my original theme from the dropdown menu, it was a terrible first impression for a professional portfolio. It was as if a ghost had possessed the CSS.

My first instinct? Smash the refresh button. Refreshing would actually fix the glitch and play the animation perfectlyβ€”but only if I stayed patiently on the tab and watched it finish! If I refreshed and immediately switched tabs again, the ghost would return.

πŸ’» Code

Here is the exact code change we made to exorcise the ghost. (You can view the full fix in this GitHub Commit)

The Buggy Code (Before):

const transition = document.startViewTransition(() => {
  html.setAttribute('data-theme', theme);
});

// If the browser aborts the transition here, the Promise rejects and execution halts forever!
transition.ready.then(() => {
  const animation = document.documentElement.animate(/* ... */);
  animation.onfinish = resolve;
});
Enter fullscreen mode Exit fullscreen mode

The Bulletproof Code (After):

let isCancelled = false;

// 1. Gracefully handle tab switching
const handleVisibility = () => {
  if (document.hidden) {
    isCancelled = true;
    html.setAttribute('data-theme', originalTheme); // Instantly restore
    document.removeEventListener('visibilitychange', handleVisibility);
  }
};
document.addEventListener('visibilitychange', handleVisibility);

// 2. Bulletproof the View Transition Promise
return new Promise(resolve => {
  try {
    const transition = document.startViewTransition(() => {
      if (!isCancelled) html.setAttribute('data-theme', theme);
    });

    transition.ready.then(() => {
      if (isCancelled) return resolve();

      const animation = document.documentElement.animate(/* ... */);
      animation.onfinish = resolve;
      animation.oncancel = resolve;

    }).catch(() => {
      // THE MAGIC FIX: If the browser aborts the transition (background tab), resolve immediately!
      resolve();
    });
  } catch (error) {
    // Fallback if startViewTransition throws synchronously
    if (!isCancelled) html.setAttribute('data-theme', theme);
    setTimeout(resolve, duration);
  }
});
Enter fullscreen mode Exit fullscreen mode

πŸ› οΈ My Improvements

My technical approach to resolving this was two-fold:

  1. State Management on Visibility Change: I added a visibilitychange event listener. If the user navigates away, we instantly cancel the sequence and forcefully restore the original theme to prevent intermediate states.
  2. Error Handling for the View Transitions API: The core issue was that browsers forcefully abort the startViewTransition when a tab is hidden to save battery. This caused the transition.ready Promise to reject. Because there was no .catch() block, the unhandled rejection caused the entire async sequence to hang permanently. By wrapping the transition in a try/catch and adding a .catch(() => resolve()) handler, we guarantee that the execution flow always continues gracefully, even if the browser interrupts it.

πŸ€– Best Use of Google AI

I couldn't figure out why this bug was only happening sometimes, and no errors were showing up in the console. So, I turned to my trusty pair programmer: Google AI integrated within the Antigravity IDE.

What's really cool about my workflow is that I chat with my AI agent completely casually in Arabic. It feels incredibly natural, like brainstorming with a senior engineer sitting right next to me. But debugging with AI isn't always a "one-shot" magic trick. It’s an iterative journey.

(Note: I interact with my AI agent natively in Arabic! For this post, I have translated our debugging conversation. I've kept the original Arabic screenshots on hand just in case the judges want to see them!)

Phase 1: The Misdiagnosis

At first, I explained the symptoms loosely to the AI:

Me: "I have another issue. Sometimes the theme changes to dark suddenly and weird things happen. I don't know what it is."

The AI analyzed the code and thought the animation feature itself was the bug (assuming it was just annoying the user). So, it enthusiastically offered a "fix" by completely disabling the showcase animation function!

I had to intervene and correct its assumption:

Me: "Bro, no, you misunderstood! The animation at the beginning is natural and fine. What's NOT natural is leaving the website open, going to another tab, and coming back to find it permanently stuck on a new theme."

Phase 2: The Deep Dive

Once I steered the AI in the right direction, it immediately dug into the mechanics of tab-switching. It realized that modern browsers aggressively pause requestAnimationFrame when a tab goes to the background.

The AI wrote the visibilitychange listener, but then we ran into another issue when I was testing it locally with my dev server.

Me: "Why does it open on a different theme every time I hit save in my editor? I have to manually refresh to get my original theme back."

The AI assumed I was just being impatient with the 1.7-second animation delay during live-reloads:

AI: "When you save, the dev server refreshes the page. The animation starts, switches to the showcase theme, and then returns. You are just manually refreshing before the 1.7 seconds finish, trapping yourself in a loop! Just wait 1.7 seconds."

Phase 3: The "Aha!" Moment

I knew I wasn't just being impatient. The UI was literally getting stuck. I pushed the AI to look deeper:

Me: "Bro, I'm not rushing. I open it, find it on a new theme, and it is completely STUCK on it. This causes problems if the user goes to another tab. Focus well, think carefully, and take your time."

That’s when the AI hit the absolute jackpot.

It didn't just hand me a snippet of code; it provided a masterclass on browser mechanics. It analyzed the View Transitions API behavior and uncovered the true root cause: to save battery and resources, modern browsers aggressively throttle or abort animations when a tab is hidden. This aggressive optimization was causing the transition.ready promise to reject silently, which hung the entire async sequence because it lacked a .catch() block.

I initially asked the AI if we could just "force" the browser to play the animation in the background anyway. Instead of blindly agreeing, the AI pushed back and explained the engineering philosophy behind this: it’s a hard performance constraint built into modern browsers. You can't fight the browser's resource management. Instead, it taught me the "Best Practice": monitor the tab's state (which led to the visibilitychange listener), gracefully catch any forced abortions (the .catch() fix), instantly restore the original UI, and respect the browser's rules.

Thanks to Google AI in the Antigravity IDE, I didn't just fix a bugβ€”I learned exactly why the browser was behaving that way under the hood. It transformed a frustrating debugging session into a profound learning experience about advanced JavaScript APIs and browser rendering mechanics. This is exactly what the future of pair programming looks like!

Top comments (0)