Adding Dark Mode to My Astro Site with viewtransitions using plain Javascript
The Button
The Astro Component for the dark / light mode switching button. Freaking love Astro!
It has a class='btn--mode-switch'
for styling, a [data-mode-button]
for interactivity and a title tag for accessibility.
The SVG Icons are from Iconship Interface Icons Collectionon SVG Repo.
And now for the part that we’ve all been waiting for:
The Javascript to Add Dark Mode Functionality to Astro
Sorry, actually, before we get to the good stuff, I just wanted to share how I am currently importing the js code.
I threw it into the Layout page.
I may end up putting it into a <script is:inline></script>
instead. I don’t know if there will be any performance gains, but inline would remove the need to load an external file. As it is, at the moment it is the only js that I have on the site. [edit: I lied, the viewtransition component is js]
“What’s that @ symbol, in your import code?”
Oh, I’m so glad you asked. I’m almost done the post to explain just that very thing. So there should be a link here… some where… some day…
Now back to the good part:
Finally, the actual Javascript Code that changes Dark and Light Mode
That’s it. Good night.
This Is Where We Break it Down
It’s business time.
In the View Transitions component, Astro exposes a few custom events for us to hook into the page transition lifecycle.
Astro View Transition Router Events
document.addEventListener("astro:before-preparation", (ev) => {
console.log("astro:before-preparation");
});
document.addEventListener("astro:after-preparation", (ev) => {
console.log("astro:after-preparation");
});
document.addEventListener("astro:before-swap", (ev) => {
console.log("astro:before-swap");
});
document.addEventListener("astro:after-swap", (ev) => {
console.log("astro:after-swap");
});
document.addEventListener("astro:page-load", (ev) => {
console.log("astro:page-load");
});
Astro Page is Loaded
I hooked into the astro:page-load
, as I will need to get and set the initial ‘mode’ value from the users preference, and the event fires on ‘initial page navigation’.
document.addEventListener("astro:page-load", (ev) => {
pageLoaded();
});
When the event "astro:page-load"
fires we call the pageLoaded()
function that will initialize the script.
I intend on adding the (future) search function to pageLoaded()
so I gave the function a generic page is loaded name. Also naming conventions are the hardest part of coding.
Error Checking and Variables
The first thing that we want to do is check to make sure that matchMedia
is actually a part of the window
object, as this feature is not supported in some older browsers. Read more about Can I Use… matchMedia.
I set MODENAME
as the local storage key name. Convention is that for static named variables we make the name all caps.
I needed to only allow the setMode()
function to save allowedModes
, since mode can only be 1 of 2 modes (light or dark), because I had a bug… the setMode(mode)
requires a mode variable and I forgot to provide one in the call setMode()
and I kept getting undefined
saved to the local storage.
const pageLoaded = () => {
if (!window.matchMedia) return;
const MODENAME = "dchmode";
const allowedModes = ["light", "dark"];
Initialize the Mood, I Mean Mode
The initMode()
function needs to be called first on every page load and only once. We check the localStorage (getMode()
) to see if the value of MODENAME
has been set.
If the value of MODENAME
is not set yet (bet jet), then we need to check the users’ dark mode preference through window.matchMedia
and set it with the setMode(mode)
.
Or getMode()
does return a value and we just continue to doMode()
.
The doMode(mode)
function takes the users preference, and applies a class .darkmode
to <html>
or removes it.
const initMode = () => {
let mode = getMode();
if (mode === null) {
mode = window.matchMedia("prefers-color-scheme: dark").matches ? "dark" : "light";
setMode(mode);
} else {
doMode(mode);
}
};
The setMode(mode)
function accepts a variable (which should only be 1 of the allowedModes
), and saves it to the local storage using the key MODENAME
.
Notice that in initMode()
and setMode(mode)
they both end by envoking doMode(mode)
.
const setMode = (mode) => {
if (!allowedModes.includes(mode)) return;
localStorage.setItem(MODENAME, mode);
doMode(mode);
};
The getMode()
function retrieves the value of MODENAME
from local storage and returns it to where the function was invoked.
const getMode = () => {
return localStorage.getItem(MODENAME);
};
The doMode(mode)
function handles the changing of the mode in the browser for the user by first, accepting the mode
then referencing the <html>
element on the page, that should only have one instance of, then applying the class="darkmode"
if the mode === 'dark'
or removing it if the mode === 'light'
.
The if statements if (html.classList.contains("darkmode"))
simply check if the html element has the class, before adding or removing it.
const doMode = (mode) => {
const html = document.querySelector("html");
if (mode === "dark") {
if (html.classList.contains("darkmode")) return;
html.classList.add("darkmode");
}
if (mode === "light") {
if (html.classList.contains("darkmode")) html.classList.remove("darkmode");
}
};
Mode Toggling
Finally, the initMode()
function is called after it has been registered into the DOM to start the process of applying the mode that matches window.matchMedia
.
The button with the attribute [data-mode-button]
is referenced and is giving an event listener for the "click"
event, that allows the user to change the mode.
when modeBtn
is clicked the changeMode()
is called
First the changeMode()
gets the previous mode using getMode()
, and oppModeOf
switches the mode to the opposite of itself, ‘light to dark’ and ‘dark to light’, which gets sent to setMode(oppModeOf[mode])
which calls doMode(mode)
and applies the new mode.
const changeMode = () => {
const mode = getMode();
const oppModeOf = {
light: "dark",
dark: "light",
};
setMode(oppModeOf[mode]);
};
initMode();
const modeBtn = document.querySelector("[data-mode-button]");
modeBtn.addEventListener("click", (e) => {
changeMode();
});
Did that explain it?
Enjoy the rest of your day.