Build a Dark Mode Theme Toggle with Qwik and Tailwind CSS, Part 2
In this tutorial we are building a dark mode theme toggle component using Qwik and Tailwind CSS. In the first part of this tutorial, we set up our project, added a simple home page, and set up dark mode styling with Tailwind. In this part, we will build the theme toggle component and add it to our home page navigation bar. It should end up looking somewhat like the dropdown at the top of this very page!
You don't have to have read Part 1 to follow along, but I will be assuming you have a basic Qwik app already set up and dark mode styles ready to go.
Table of Contents
- Introduction
- Setup Theme Change Functionality
- Building the Theme Toggle Component
- Setting Theme On Load
- Conclusion
Introduction
Let's get started by reviewing requirements for the theme toggle component:
- It should be a dropdown menu that allows the user to select a theme.
- It should display the current theme.
- It should update the theme when a new theme is selected.
- Users should be able to select a light theme, dark theme, or use their system theme.
- The website should remember the user's theme preference between visits and load with their preferred theme.
Over the course of this tutorial we'll build out a custom dropdown menu that will cover the first four requirements then we'll see how to set the user's preferred theme on load using the browser's localStorage
.
Setup Theme Change Functionality
Before we build the theme toggle component, we need to set up the functionality to change the theme. We will do this by creating a few helper functions that will handle setting and getting the theme from localStorage
. There are a number of ways to do this, and I don't claim mine to be the best, but it works well for me and it is simple.
First create a new file in your components
directory under theme-toggle/themeHelpers.ts
.
export const LOCAL_STORAGE_KEY = "theme"
export const SYSTEM = "system"
export const DARK = "dark"
export const LIGHT = "light"
export type ThemeOption = typeof SYSTEM | typeof LIGHT | typeof DARK
function applyLightTheme() {
document.documentElement.classList.remove(DARK)
}
function applyDarkTheme() {
document.documentElement.classList.add(DARK)
}
function applySystemTheme() {
window.matchMedia("(prefers-color-scheme: dark)").matches
? applyDarkTheme()
: applyLightTheme()
}
export function setTheme(theme: ThemeOption) {
// Save theme choice
localStorage.setItem(LOCAL_STORAGE_KEY, theme)
// Apply theme
switch (theme) {
case DARK:
applyDarkTheme()
break
case LIGHT:
applyLightTheme()
break
case SYSTEM:
applySystemTheme()
break
}
}
As we saw in the previous post, we can add and remove the Tailwind dark
class from the documentElement
to switch between light and dark themes. This is accomplished with the applyLightTheme
and applyDarkTheme
functions. The applySystemTheme
function uses the prefers-color-scheme
media query to determine if the user's system is set to dark mode. If it is, the dark theme is applied, otherwise the light theme is applied by removing the "dark" class from the html
element.
These functions are all used in the setTheme
function, which takes a ThemeOption
as an argument, saves the theme to localStorage
, and applies the theme to the document. We will use the saved value in localStorage
to set the user's preferred theme on load later on.
Building the Theme Toggle Component
Main Structure
Let's look at the component structure of the theme toggle.
<Dropdown>
<MenuButton />
<DropdownMenu>
<DropdownMenuItem /> // System theme option
<DropdownMenuItem /> // Light theme option
<DropdownMenuItem /> // Dark theme option
</DropdownMenu>
</Dropdown>
The Dropdown
component will be the container for the entire dropdown menu. It will contain a MenuButton
component that will display the current theme and open/close the dropdown menu. The DropdownMenu
component is the container for the DropdownMenuItem
components that when selected, will update the theme.
This will all be returned from a single ThemeToggle
component that will control the top level logic.
Get started by creating a new file in your theme-toggle
directory called index.tsx
. Export a basic component called ThemeToggle
. We'll build this out as we go.
import { component$ } from "@builder.io/qwik"
export const ThemeToggle = component$(() => {
return <div>Theme Toggle</div>
})
We went over Qwik's component$
in the previous post. As a refresher, this is how we define components in Qwik. You will see it in every component we build. The dollar sign can be thought of as lazy loading boundary that allows Qwik to bundle components into separate JavaScript chunks to send to the browser. This is an oversimplification, but will suffice for now. If you would like to read more, I encourage you to check out the Qwik documentation.
Dropdown Container
Next, let's build out the Dropdown
component. Create a new file in the theme-toggle
directory called dropdown.tsx
. Add the following code.
import type { QRL } from "@builder.io/qwik"
import { Slot, component$ } from "@builder.io/qwik"
interface DropdownProps {
closeDropdown: QRL<() => void>
}
export const Dropdown = component$(({ closeDropdown }: DropdownProps) => {
return (
<div
class="relative inline-block dark:text-zinc-50"
document:onClick$={(event, currentTarget) => {
const clicked = event.target as HTMLElement
if (!currentTarget.contains(clicked)) {
closeDropdown()
}
}}
>
<Slot />
</div>
)
})
There is a lot to go over here, so let's break it down.
First we define the props. This interface looks just like props you might create in a React component when using TypeScript, with the exception of one Qwik specific type, QRL
. This is a Qwik URL, "a particular form of URL that Qwik uses to lazy load content". These are left as attributes in the HTML delivered to the client and they tell Qwik handlers where to load the given code from. In functions provided by Qwik that contain a $
, QRLs are created automatically. However, when passing your own functions across lazy loading boundaries, you must create them yourself. Qwik makes this easy, as we will see later.
The structure of the component is a div
with a Slot
. We covered slots previously. They are the Qwik equivalent of React's children
. The Slot
component will render the children of the Dropdown
component.
The styles are not particularly interesting here, they just set the stage for child styles and creates a default dark mode text color.
The most interesting part of this component is the document:onClick$
event handler. This handler shows us three aspects of Qwik event handling. The most important, and most commonly seen, is the onClick$
following the colon. This is how you define any browser event in Qwik on HTML elements. If you wanted to define some action to occur on keydown
, you would use onKeyDown$
. Again, the $
means that the handlers are loaded on the client only when needed.
The second thing to note is the document:
prefix. This tells Qwik to listen for the event, in this case a click, on the document
object rather than the element itself. You can also use window:
in the same way. This is useful for global events like clicks or key presses.
Finally, it is worth discussing the currentTarget
argument of the handler. This is a reference to the element that the event handler is attached to. In this case, it is the div
element. We need this because of the asynchronous nature of loading client code in Qwik. We cannot use event.currentTarget
directly, so Qwik provides this reference for us as the optional second argument to the handler. It is also worth noting that you cannot use event.preventDefault()
for the same reason. Instead you would use a boolean attribute on the element that looks like preventdefault:{eventName}
.
Comparing currentTarget
with the event target, the handler will close the dropdown when a user clicks outside of it. This is something people expect to work, and does not happen automatically. We pass in the closeDropdown
function to the Dropdown
component rather than defining it here because we will define the open/close state at the top level component.
To dive deeper on Qwik event handling, see the docs.
Menu Button
We'll be using some icons in the menu button to visually indicate the different options. The easiest way to access many different icons is through the qwikest/icons
package. Add it to the project with the following command.
pnpm add @qwikest/icons
This allows us to use a number of popular icon libraries in our app. I will be using just the Tailwind Heroicons set, but feel free to explore other options. If you prefer to avoid using extra dependencies, you can also get the SVGs directly from the Heroicons website.
Now create a new file in the theme-toggle
directory called menu-button.tsx
. Add the following code.
import type { ButtonHTMLAttributes } from "@builder.io/qwik"
import { component$ } from "@builder.io/qwik"
import {
HiChevronDownOutline,
HiChevronUpOutline,
HiComputerDesktopOutline,
HiMoonOutline,
HiSunOutline,
} from "@qwikest/icons/heroicons"
import { DARK, LIGHT, SYSTEM, type ThemeOption } from "./themeHelpers"
interface MenuButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
id: string
open: boolean
selection?: ThemeOption
}
export const MenuButton = component$(
({ id, open, selection, ...props }: MenuButtonProps) => {
return (
<button
type="button"
class="block"
id={id}
aria-expanded={open}
aria-haspopup="menu"
{...props}
>
<span class="flex flex-row items-center justify-center gap-2">
Theme
<div class="text-lg">
{selection === SYSTEM ? (
<HiComputerDesktopOutline />
) : selection === LIGHT ? (
<HiSunOutline />
) : selection === DARK ? (
<HiMoonOutline />
) : null}
</div>
{open ? (
<HiChevronUpOutline class="text-md" />
) : (
<HiChevronDownOutline class="text-md" />
)}
</span>
</button>
)
},
)
Starting from the top, we have our props. We pass an ID for the button, whether the dropdown is open, and the current theme selection. We also allow any props accepted by the HTML Button element. We are passing in the ID here so that we can use it separately in the DropdownMenu
component we'll create later to set the aria-labelledby
attribute. This is also why we have the text "Theme" in the button. It serves as the label for screen readers.
Next we have the button itself. It is a simple button with some padding. The aria-expanded
and aria-haspopup
attributes are used to indicate to screen readers that this button controls a dropdown menu. The aria-expanded
attribute is set to the value of the open
prop, which will be managed by the ThemeToggle
component. The aria-haspopup
attribute is set to "menu" to indicate that the button opens a menu.
Inside the button, we have a span
that contains the text "Theme" and an icon. The icon is determined by the selection
prop. If the selection is SYSTEM
, we show a computer icon. If the selection is LIGHT
, we show a sun icon. If the selection is DARK
, we show a moon icon. If the selection is none of these, we show nothing. Finally, we have the chevron icon. This icon will indicate to the user that the dropdown menu is open or closed. If the dropdown is open, we show a chevron pointing up. If the dropdown is closed, we show a chevron pointing down. The styles on the span
ensure these elements are centered and spaced nicely in a row. The styles on the icons set the width and height based on Tailwind's sizing system.
Dropdown Menu Container
Next, let's build out the DropdownMenu
component. Create a new file in the theme-toggle
directory called dropdown-menu.tsx
. This is a container that will hold the menu items. It is fairly simple, but we'll be adding more styling than we have seen previously. This will highlight one of the potential negatives of Tailwind, long class names. But remember, the Prettier plugin for Tailwind will help you keep your class names organized, and we only write them once in a component definition.
import { Slot, component$ } from "@builder.io/qwik"
interface DropdownMenuProps {
open: boolean
btnId: string
}
export const DropdownMenu = component$(({ open, btnId }: DropdownMenuProps) => {
return (
<ul
class="animate-drop-down-in absolute right-0 z-10 m-0 mt-1 w-40 origin-top-right list-none rounded-lg bg-white p-0 shadow-sm ring-1 ring-black ring-opacity-5 focus:outline-none dark:bg-black/90 dark:shadow-none dark:ring-zinc-700"
role="menu"
hidden={!open}
aria-orientation="vertical"
aria-labelledby={btnId}
>
<Slot />
</ul>
)
})
Most of the elements of this component we have seen before, so I'll highlight the styles. The eagle-eyed among you may have noticed that animate-drop-down-in
is not a Tailwind class. This is a custom class that I have defined in my tailwind.config.js
file which will animate the opening of the menu. You can define custom classes in Tailwind by adding them to the theme.extend
object in the config file. Here is what mine looks like.
// ...
theme: {
// ...
extend: {
animation: {
"drop-down-in": "drop-down-fade-in 0.25s ease-out",
},
keyframes: {
"drop-down-fade-in": {
"0%": { opacity: "0", transform: "scale(0.95)" },
"100%": { opacity: "1", transform: "scale(1)" },
},
},
}
}
// ...
This animation changes the opacity from invisible to fully opaque and subtly scales the menu up from 95% to 100% size. You can adjust the timing and easing to your liking.
The other styles worth mentioning here set the positioning to the right of the button, set the width, and ensure the menu is above other elements on the page. If you are interested, this is a good opportunity to explore the Tailwind documentation to see what each class does.
Dropdown Menu Items
Before putting everything together, we need to create the DropdownMenuItem
component to display our theme options.
Create a new file in the theme-toggle
directory called dropdown-item.tsx
. Add the following code.
import { Slot, component$, type ButtonHTMLAttributes } from "@builder.io/qwik"
type DropdownItemProps = Omit<
ButtonHTMLAttributes<HTMLButtonElement>,
"type" | "role" | "class"
>
export const DropdownItem = component$((props: DropdownItemProps) => {
return (
<li>
<button
type="button"
role="menuitem"
class="block w-full px-4 py-2 text-left hover:bg-gray-100 focus:bg-gray-100 focus:outline-none active:bg-gray-200 dark:hover:bg-zinc-800 dark:focus:bg-zinc-800 dark:active:bg-zinc-700"
{...props}
>
<span class="flex items-center gap-1 text-sm">
<div class="mr-2 text-lg">
<Slot name="icon" />
</div>
<Slot name="text" />
</span>
</button>
</li>
)
})
Each option is a list item with a button inside. We allow all button HTML attributes to be passed in as props except for type
, role
, and class
, which we are specifically defining within the component. The styles set up some basic display properties as well as some hover and active styles for both light and dark modes.
Within the button we have a span
that will display an icon and some text. This highlights the use of named slots. While not required when using multiple slots, I prefer it because it helps make clear what each slot is intended for. We'll see how to assign elements to each slot shortly.
Putting It All Together
State
We are now ready to set up the primary ThemeToggle
component. To start things off, we need to set up some state used in the components above. We need to know what theme is selected, if the dropdown is open or not, and we need an HTML id we can pass to the button and the dropdown menu for accessibility purposes. Open the index.tsx
file in the theme-toggle
directory and add the following code to the top of the function.
import { $, component$, useId, useSignal } from "@builder.io/qwik"
import type { ThemeOption } from "~/components/theme-toggle/themeHelpers"
import {
DARK,
LIGHT,
SYSTEM,
setTheme,
} from "~/components/theme-toggle/themeHelpers"
// ...
export const ThemeToggle = component$(() => {
// Start with light theme for now
const selectedTheme = useSignal<ThemeOption>(LIGHT)
const isOpen = useSignal(false)
const id = useId()
// ...
})
Signals are how Qwik manages state. You can use either the useSignal
hook, which is an object with a single .value
property, or the useStore
hook, which is an object that can have multiple values. Note that to track arrays or object, you must utilize useStore
. useSignal
can only track basic types.
These are similar to React's useState
except that you access the values directly on the object instead of using a separate setter function. If you prefer creating a method to update state, you can do that within the useStore
object, which you can read more about in the docs linked above.
In addition to these methods, there are several other options for state management including context, creating computed state, and more. We only need some simple state here, so I've opted to use the most basic method. You could easily swap out my code for useStore
or another method if you prefer. When complete, I'd encourage you to try it out as a learning exercise.
We also need to discuss useId
. This is a hook that provides a unique and consistent ID across the server and client environments. Because Qwik operates across both, it is important to have a way to access elements by ID that works in both.
Handle Changes
Next, we need to set up the functions that will handle changes to the theme and the dropdown menu. Add the following code to the ThemeToggle
component below the state.
// ...
const closeDropdown = $(() => {
isOpen.value = false
})
const handleThemeChange = $((theme: ThemeOption) => {
setTheme(theme)
selectedTheme.value = theme
})
// ...
The logic here is simple, one function closes the dropdown and the other updates the theme. What is unique to Qwik is the $
function that wraps both of these. I've covered this briefly before, but to review, this helps Qwik break down our application into many small pieces. It is what allows Qwik to lazy load components and functions and improve load time performance. It is covered in depth in the Qwik docs, if you want to learn more.
We saw earlier how the Dropdown
component takes a function wrapped in a QRL
. This is how you create a function with a QRL.
Component
Finally, let's use all of the above to put together the full dropdown. Replace the return value with the following code.
// ...
import {
HiComputerDesktopOutline,
HiMoonOutline,
HiSunOutline,
} from "@qwikest/icons/heroicons"
import { Dropdown } from "./dropdown"
import { DropdownItem } from "./dropdown-item"
import { DropdownMenu } from "./dropdown-menu"
import { MenuButton } from "./menu-button"
// ...
return (
<Dropdown closeDropdown={closeDropdown}>
<MenuButton
id={id}
selection={selectedTheme.value}
open={isOpen.value}
onClick$={() => {
isOpen.value = !isOpen.value
}}
/>
<DropdownMenu btnId={id} open={isOpen.value}>
<DropdownItem
onClick$={[closeDropdown, $(() => handleThemeChange(SYSTEM))]}
>
<HiComputerDesktopOutline q:slot="icon" />
<div q:slot="text">System</div>
</DropdownItem>
<DropdownItem
onClick$={[closeDropdown, $(() => handleThemeChange(LIGHT))]}
>
<HiSunOutline q:slot="icon" />
<div q:slot="text">Light</div>
</DropdownItem>
<DropdownItem
onClick$={[closeDropdown, $(() => handleThemeChange(DARK))]}
>
<HiMoonOutline q:slot="icon" />
<div q:slot="text">Dark</div>
</DropdownItem>
</DropdownMenu>
</Dropdown>
)
// ...
Ok, most of this fairly straightforward, but there are a few things to note regarding how you pass functions, and how you select named slots for children to render in. Let's start with the later. In our DropdownItem
component, we have two named slots, icon
and text
. To assign elements to these slots, we use the q:slot
attribute. This is a Qwik specific attribute.
Next, you'll notice there are several ways to pass functions as props and handlers. First is the onClick$
handler on the MenuButton
component. I've opted to define a function directly here rather than define it with the others above. You do not need to do this, but it serves as a reference to show you can. Note that in this case we do not wrap the enclosed function in a dollar sign, the event handler does this automatically when given a single function.
Then we see some onClick$
s in the DropdownItem
components that are given a list of functions. In Qwik, this is how you can pass multiple functions to a single event. Here when each item is clicked, we'll close the dropdown and then change the theme.
In the second function in these lists, we are creating a new function that calls one of our previously defined functions. This is similar to React when you need to call a function that requires arguments. However, in this case we need to wrap the function in a $
to create a QRL, because Qwik will need a reference to this function in place of the actual one when this list is loaded on the client. Alternatively, you could just define a new function directly and call both of the functions in the list within that. Any option will work, so use whatever is best for your specific situation.
Add To NavBar
Let's see how this thing works! Open routes/layout.tsx
and add the ThemeToggle
component to the NavBar
component.
// nav bar ul
<li class="px-4 py-3">
<ThemeToggle />
</li>
// ...
You should see something like the image below. And it works! Well... sort of.
If you select between the light and dark modes, you should see the theme change. But the system theme will just keep whatever is currently the theme you selected. Also if you refresh, the website does not remember your previous theme. We'll cover these issues in the next section.
However, if you don't want to use the system theme and don't care if the website remembers your preference, then you are done! Just remove references to the SYSTEM
theme and the localStorage
functions and you are good to go.
Setting Theme On Load
With theme management, it is important that the styles applied on load are done immediately so that there is no flash of default styles before the correct styles are applied. In Qwik, normally things are loaded asynchronously, so we need to make sure that the theme is set before the page is rendered to avoid this. Luckily, there is an escape hatch. We need to add a script tag in the root.tsx
file in the src
directory. It will look something like below, where the code sets the initial theme and sets up any event listeners.
<script
dangerouslySetInnerHTML={`
// code as a string
`}
></script>
dangerouslySetInnerHTML
is a Qwik specific attribute. From the docs:
Due to cross-site-scripting (XSS) possibilities when rendering untrustworthy content, you must use dangerouslySetInnerHTML as a reminder that this operation might be dangerous.
Now... I really don't like writing code in a string like this. It feels hacky and it looks ugly, just check out the example given in the Qwik docs to see what I mean. But there is a better way!
Well, kind of. When putting this post together, I thought I had the perfect solution. Qwik uses Vite as a build tool, and I tried to use a Vite specific feature to import the raw text of a code file and pass that to the script tag. For example,
import setInitialThemeAsString from "./utils/setInitialTheme?raw"
Where the import value is the text contents of the setInitialTheme.js
file, which contained the logic to setup theming. This worked great in development, but when trying a production build, it failed with an error I was unable to resolve from a Vite dependency, Rollup. If you don't care about running a production build and are just playing around, this is a nice solution. Otherwise, there appears to be no choice but writing code as a string.
Let's see the code we'll write with correct color syntax highlighting first so we can more easily see what is going on.
const LOCAL_STORAGE_KEY = "theme"
const SYSTEM = "system"
const DARK = "dark"
const LIGHT = "light"
const watcher = window.matchMedia("(prefers-color-scheme: dark)")
function setSystemTheme() {
watcher.matches
? document.documentElement.classList.add(DARK)
: document.documentElement.classList.remove(DARK)
}
// Listen for system theme changes
watcher.onchange = () => {
console.log("System theme changed")
const theme = localStorage.getItem(LOCAL_STORAGE_KEY)
if (theme === SYSTEM) {
setSystemTheme()
}
}
// Set initial theme
const theme = localStorage.getItem(LOCAL_STORAGE_KEY)
switch (theme) {
case DARK:
document.documentElement.classList.add(DARK)
break
case LIGHT:
document.documentElement.classList.remove(DARK)
break
case SYSTEM:
default:
setSystemTheme()
localStorage.setItem(LOCAL_STORAGE_KEY, SYSTEM)
break
}
You will immediately notice some duplication from the themeHelpers.ts
file. I initially tried to export everything from this code - when it was in a separate file - and import into the dropdown files, but this caused errors due to the use of document
, window
, and localStorage
, which are all only available in the browser. In the server environment these are undefined and cause errors when imported. So the duplication is a necessary evil. Now let's break down what this code does.
Following the constants, we set up a watcher
on the media query for prefers-color-scheme: dark
. The matchMedia
method returns a live object that stays up to date with the media query. This means that every time we .match
on it, the result will be accurate to the moment, and we can set up event listeners on when the preference changes. This is how we will keep up to date with the system theme.
The setSystemTheme
function checks the watcher
to see if the system is set to dark mode and applies a dark
class on the html
element if it is, or ensures the class is removed if it is not.
Next we set an onchange
listener on the watcher
. This will only be used when the theme is set to system, because otherwise the user is explicitly overriding the system theme value. So we check if the stored theme (set up later) is SYSTEM
, and if it is, we call setSystemTheme
to update it to the new value.
Finally, we set the initial theme. This code is not contained in a function so it will run when the file is loaded. It is simply fetching the initial theme from localStorage
and applying the appropriate class to the html
element. We make the SYSTEM
case the default as well, and if no item exists in localStorage
, we set it to SYSTEM
to start. As users select themes in the dropdown, the localStorage
value will be updated and that theme will be picked up on load.
Now we need to turn this into a string in the root.tsx
file. Open that file and add the following constant under the imports.
const themeStartupCode = `
// Code from above...
`
Now pass this constant to the script tag:
<head>
// ...
<script dangerouslySetInnerHTML={themeStartupCode}></script>
</head>
If you save and refresh the page, you should now see the theme you previously selected. If you change your system theme, the website should update to reflect that. And if you select a theme in the dropdown, the website should update to reflect that as well. But there is still one problem. On refresh, the icon shown in the dropdown button is always the system icon. Thankfully this is an easy fix, and allows us to see one last feature of Qwik.
Open the components/theme-toggle/index.tsx
file. First, remove SYSTEM
from the selectedTheme
state.
const selectedTheme = useSignal<ThemeOption>()
Then we will use the Qwik useVisibleTask$
hook to fetch the localStorage
theme value and choose an appropriate icon.
In Qwik, useVisibleTask$
should be used when a task needs to run only on the browser and after rendering. In general, you should prefer useTask$
but in this case we are using a browser specific API, localStorage
, which is not available on the server. The Qwik team feels strongly enough about this that they added an ES Lint rule to throw a warning when using useVisibleTask$
instead of useTask$
. You'll see the comment to disable the warning below, which they say to add if you need to use this hook.
There is a lot to learn when it comes to Qwik lifecycle hooks, so I encourage you to check out the docs.
// eslint-disable-next-line qwik/no-use-visible-task
useVisibleTask$(() => {
const theme = localStorage.getItem(LOCAL_STORAGE_KEY)
if (theme) {
selectedTheme.value = theme as ThemeOption
}
})
But... There is still one more issue.
On initial load, the icon still doesn't show up. But click on the dropdown, and you will see it updates immediately. This is a great highlight of Qwik's efforts to improve performance. In most circumstances, you would not want this JavaScript to load until it is needed. However, Qwik does not know that in this case, it is required before any user interaction. We can tell Qwik to run this code eagerly by passing a second argument to the hook, an object with a strategy
property set to "document-ready"
.
// eslint-disable-next-line qwik/no-use-visible-task
useVisibleTask$(
() => {
const theme = localStorage.getItem(LOCAL_STORAGE_KEY)
if (theme) {
selectedTheme.value = theme as ThemeOption
}
},
// For icon to show up on load, needs to run eagerly
{ strategy: "document-ready" },
)
Finally, the correct icon is used on initial load of the website and we are done!
Conclusion
You will still notice a small visual flicker on the icon as it takes a split second to load. I have not yet found a good way to prevent this. To load the icon, we need the dropdown component to be rendered. We can only do that after the page is loaded, so there is always going to be a small delay.
Realistically, on a fully featured website, this will be a minor issue and ultimately faster than any data fetching you might do from the client. Users probably will not notice or care much. But I also don't know your users. If you think your users will be upset by this then you may need to find alternatives to this solution.
One possible way to fix the flicker could be to use a backend database to store user preferences and load them on the server in the pre-rendering process. If you try this out, I would be interested to hear how it works!
Thanks for sticking with me through this tutorial, we've covered a lot of ground. I hope you have learned something new and found this content interesting. Have fun coding!