Astro View Transitions and Dark Mode
By Flavio Copes
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. When I wrote this post, this blog used them, and clicking a menu item moved you to the next page with a very smooth transition.
The site is static, but navigation feels smooth like in a single-page application.
How do you turn on View Transitions in Astro 7?
In Astro 7 you turn on this kind of navigation with the <ClientRouter /> component. Import it from astro:transitions and add it to the <head> of the layout all your pages share:
---
import { ClientRouter } from 'astro:transitions'
---
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>My site</title>
<ClientRouter />
</head>
<body>
<slot />
</body>
</html>
Put it in the shared layout rather than in a single page, so every navigation goes through the router.
Older tutorials use a <ViewTransitions /> component instead. Astro 5 renamed it to <ClientRouter /> and Astro 6 removed the old name, so swap the import when you upgrade an older project.
The dark mode problem
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.
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 if we hook the theme there, we get a brief white flash on every navigation.
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>
If the visitor never touched the toggle, session storage is empty, so we fall back to the OS preference with 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. Put it in the <head> of your layout, and it also runs before the first paint on the initial load.
Want me to talk about your product? You can sponsor this site.
Related posts about astro: