Astro View Transitions and Dark Mode

By

Learn how to fix dark mode resetting to light during Astro View Transitions, re-applying the theme from session storage on the astro:after-swap event.

~~~

If your dark mode resets to light when navigating with Astro View Transitions, the fix is to re-apply the theme on the astro:after-swap event. Run the same function that sets the theme on first load, again every time that event fires.

Here’s the full story.

View Transitions in Astro are amazing, you can see them at work on this blog by clicking a menu item, for example. Very smooth transition to other pages of the site, it’s great.

It’s a static site, but navigation feels smooth like in a single-page application.

I noticed a problem when I implemented dark mode in a new theme. Previously I just used the OS default, now I also have a new toggle to choose the mode you prefer.

But, problem: since it was loading the state from session storage in the browser, going to another page reset that preference to light mode.

Why does the theme reset?

My theme works by adding a dark class to the html element. An inline script reads the preference from session storage on page load and adds the class.

With View Transitions, clicking a link doesn’t do a full page load. Astro fetches the new page and swaps it into the current document.

The swapped-in page is the one the server rendered, and the server renders the light theme. It has no dark class. And the script that added the class already ran, on the previous page, so nothing re-applies it.

Result: every navigation flips you back to light mode.

The fix: astro:after-swap

Astro fires a series of events during a view transition. The one we want is astro:after-swap: it fires right after the new page replaces the old one, but before the new page is painted.

That “before it is painted” part matters. If we set the class at that point, the user never sees a flash of the light theme. A later event like astro:page-load fires after rendering, so hooking the theme there shows a brief white flash on every navigation. That’s the pitfall to avoid.

So I had to call the function that gets the dark mode preference from session storage also when the page is swapped with a new one in a view transition.

Here’s a link to the docs: https://docs.astro.build/en/guides/view-transitions/#astroafter-swap

<script is:inline>
  const setDarkMode = () => {
    if (typeof window !== "undefined") {
      const isSystemColorSchemeDark = window.matchMedia(
        "(prefers-color-scheme: dark)"
      ).matches
      const storageTheme = sessionStorage.getItem("theme")
      if (!storageTheme && isSystemColorSchemeDark) {
        document.documentElement.classList.add("dark")
        document.head.children.namedItem("theme-color").content = "#262626"
      } else if (storageTheme === "dark") {
        document.documentElement.classList.add("dark")
        document.head.children.namedItem("theme-color").content = "#262626"
      } else {
        // we already server render light theme
        document.head.children.namedItem("theme-color").content = "#ffffff"
      }
    }
  }

  // Runs on initial navigation
  setDarkMode()

  // Runs on view transitions navigation
  document.addEventListener('astro:after-swap', setDarkMode)
</script>

A few notes on the code. If the visitor never touched the toggle, session storage is empty, and we fall back to the OS preference via matchMedia(). If they chose dark, we apply it. In both dark cases we also update the theme-color meta tag, so the browser UI (like the mobile address bar) matches the page background.

The script is marked is:inline so Astro leaves it in the page as-is, and it runs before the first paint on the initial load too.

Tagged: Astro · All topics
~~~

Related posts about astro: