Get Rid of Theme Flicker
Part of the Astro series
As I wrote yesterday, I celebrated John Siracusa’s every-five-year Hypercritical t-shirt sale with a new Hypercritical Gold Theme for this site. As with my previous dark/light mode configuration, toggling themes is done with a little icon button at the bottom of the menu that can look like a sun (when in light mode), a moon (when in dark mode), and now a 128k Mac (when in Hypercritical Gold mode).
Theme toggling can cause flickering issues when pages load. The symptom is that when you click a link to go to another page on the site, you see a flash of the wrong colors and then the page quickly switches to your chosen theme colors. This is because when the default colors load first, and then the browser realizes you’ve set the theme to something else and loads the correct colors. I had this issue myself, but I didn’t really notice it when I just had light/dark modes, probably because I was almost always in dark mode. I did notice it right away when I added Hypercritical Gold.
I found two articles addressing this issue, one of which was even written from an Astro perspective.
Prevent dark mode from flickering on page load in Astrojs
Light/dark mode: avoid flickering on reload - DEV Community
They both basically use exactly the same technique – early on in the page, either in the <head>
section or early in the <body>
section, have an inline JavaScript that checks your preferred theme setting by seeing what your localStorage
setting for your theme choice is.
As it happens, I was already doing this. But I was doing it in my menu component, which in turn is called by my Base.astro base layout. Also my script was declared as <script>
instead of <script is:inline>
, which in Astro means it was getting bundled by Vite instead of loading directly inline where it was declared. The result of these two factors meant the timing of the script checking the visitor’s theme preference was off, and theme flicker was the result.
My first attempt at getting rid of theme flicker was changing the script to use the is:inline
directive, but this caused a problem. Now the script couldn’t find the elements it referred to by ID, namely the theme toggle icons. My assumption is this is because Astro was modifying the element names as part of how it bundled up and rendered the component, but with the JavaScript now being unmodified and unprocessed by Astro, it no longer knew what those were now called.
Because of this, I decided to nuke the menu component and move everything in it into Base.astro. I moved it as is, with the JavaScript above the html for the menu elements, and the script using the is:inline
directive. Now I had a new variation of the JavaScript can’t find html elements issue. For fun, I tried moving the JavaScript down below the menu html, hoping it was still high enough in the page to load the theme quick enough to avoid theme flicker. It seems to have worked – I don’t see theme flicker now while using Hypercritical Gold or Light themes. These are the two themes that would show flicker, because Dark theme is the default.
To recap, I no longer have a Menu.astro
component. My menu is now in my Base.astro
template. The highlighted part is the JavaScript dealing with loading the correct theme based on the user’s preference, and changing themes when the toggle icon is clicked or tapped.
The moral of the story is, grab the user theme preference and set the theme early in the page load lifecycle. With Astro, that means understanding how Astro is optimizing your layouts, components, CSS, and scripts.
In my case, because the JavaScript is loaded above the part of the base template that loads the page content, it seems to work timing-wise to load the theme quickly enough to avoid theme flicker.