Theme toggle button with Alpinejs
So I was going to create the ability to switch themes. It is easier said than done.
You may have noticed already that my website has light and dark mode. It was based on the device settings whether you get the light or dark theme. But I wanted to change that. A button that can toggle between dark and light and a system mode to use your device settings as before. So I had two requirements.
- Button - The button has to have 3 states (light/dark/system) where the icon has to be different on each state.
- Alpinejs - I have to use alpinejs and not plain old javascript.
I started with the button styling which was quite easy. It looks like this:
<button class="p-2 outline-none focus-visible:ring focus-visible:ring-blue-500">
<span class="sr-only">Toggle theme</span>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6" aria-hidden="true" @click.prevent="toTheme('dark')" x-cloak x-show="theme == 'light'">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386-1.591 1.591M21 12h-2.25m-.386 6.364-1.591-1.591M12 18.75V21m-4.773-4.227-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6" aria-hidden="true" @click.prevent="toTheme('system')" x-cloak x-show="theme == 'dark'">
<path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.72 9.72 0 0 1 18 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 0 0 3 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 0 0 9.002-5.998Z" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6" aria-hidden="true" @click.prevent="toTheme('light')" x-cloak x-show="theme == 'system'">
<path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
</svg>
</button>
Now what is happening at the alpinejs directives. First I set a click handler with @click
and define a function to call when the user clicks the svg/button. Then we set x-cloak to hide the svg before the javascript is loaded. x-show
is a directive that accept a boolean whether to show the element or not. We set that on each svg and show the correct svg based on the theme variable.
Then we need to create a alpine component. We do that in de body element because in the body we have to set the dark
class. It is done with:
<body class="antialiased bg-gray-100 text-gray-900 dark:bg-gray-900 dark:text-gray-100"
:class="{ dark: theme == 'system' ? window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches : theme == 'dark' }"
x-data="{
theme: undefined,
init() {
this.theme = window.localStorage.getItem('theme') || 'system'
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', event => {
this.theme = event.matches ? 'dark' : 'light';
});
},
toTheme(value) {
this.theme = value
window.localStorage.setItem('theme', value)
}
}">
Let's begin with the x-data directive. We create a property to hold the theme. By default it is undefined
. Below is the init function. This init function is special because alpinejs will call the init function whenever the component is created. Here we set the theme based on the localstorage value or set it to 'system'
when the localstorage value does not exist. Then we add a event listener on the matchMedia. Everytime you change the color scheme, this listener will be called and set the theme property accordingly. Below we set the toTheme()
function which set the theme property and set the localstorage to the value. This value is coming from the click event on the svg element.
And that is how I've done it. I think this is a awesome solution for a theme toggle button. And also made with alpinejs.