Cloning Google Font's Light/Dark Mode Toggle

I was following along with this one, not really being a tutorial, but there are certainly things to learn in this video, anyway. I was doing this for fun on a HUGO website I'm playing with. And I pretty much cloned what Kevin did, but I can't get the browser to remember my prefers-color-sheme, no matter what I do it will always switch to dark, which I am using as the main theme for the site. And I've checked all instances of it that I have set up with Hugo in css and golang html. I don't seem to have done anything different than Kevin. The only thing is that I'm on a pretty old 2013 Macbook Pro running OSX 10.14.6. On chrome and yes, everything on this system is themed dark. Google fonts does recognize my preference, so theirs kind of seems to work. Any clues/ ideas as to what might be causing this for me would be great.
28 Replies
Jochem
Jochem•17mo ago
without code it's impossible to tell
Peacock Plume
Peacock Plume•17mo ago
I see, should I post code here? Not sure if that's wanted.
Jochem
Jochem•17mo ago
yup, in this thread. Ideally in a codepen that demonstrates the problem, or otherwise in a code block. Check out #How To Ask Good Questions thread pinned to the top of this channel too
Peacock Plume
Peacock Plume•17mo ago
Well I could setup a codepen for it, but I'd have to create that, and I doubt it will cause the same mistake, it will probably work as I think it's pretty much an exact copy of Kevin's code, only difference is my svg probably and some minor things. So I'll post it here for now in code blocks and let you guys have a look whenever you have the time of course if you can find my issue.
Jochem
Jochem•17mo ago
just make sure to copy your code, and not re-copy the code from Kevin's video, cause if you made a small mistake copying earlier that mistake won't be in a second copy
Peacock Plume
Peacock Plume•17mo ago
for sure I wouldn't do that
Jochem
Jochem•17mo ago
also, it's really really hard to diagnose a problem if you can't see it breaking. so if your "svg probably and some minor things" are what's causing the issue, we'd need to see those too
Peacock Plume
Peacock Plume•17mo ago
I'm copying from my vscode setup including the HUGO stuff that relates to it Of course I was going to post that as well, all of it. And it's not a problem if you can't find an issue with what I'll be posting, in the end I might set up a code pen as well, but not right now. So starting with the svg as that's also what Kevin started with and the difference is that I made mine in Affinity Designer.
<h1>Click the button to switch themes</h1>
<button id="theme-toggle" aria-label="Switch to light theme">
<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<g>
<path class="toggle-sun" d="M16,0L12.107,4.017L6.595,3.056L5.807,8.594L0.783,11.056L3.401,16L0.783,20.944L5.807,23.406L6.595,28.944L12.107,27.983L16,32L19.893,27.983L25.405,28.944L26.193,23.406L31.217,20.944L28.599,16L31.217,11.056L26.193,8.594L25.405,3.056L19.893,4.017L16,0ZM16,8.773C19.989,8.773 23.227,12.011 23.227,16C23.227,19.989 19.989,23.227 16,23.227C12.011,23.227 8.773,19.989 8.773,16C8.773,12.011 12.011,8.773 16,8.773Z"/>
</g>
<g>
<circle class="toggle-circle" cx="16" cy="16" r="5.511"/>
</g>
</svg>
</button>
<h1>Click the button to switch themes</h1>
<button id="theme-toggle" aria-label="Switch to light theme">
<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<g>
<path class="toggle-sun" d="M16,0L12.107,4.017L6.595,3.056L5.807,8.594L0.783,11.056L3.401,16L0.783,20.944L5.807,23.406L6.595,28.944L12.107,27.983L16,32L19.893,27.983L25.405,28.944L26.193,23.406L31.217,20.944L28.599,16L31.217,11.056L26.193,8.594L25.405,3.056L19.893,4.017L16,0ZM16,8.773C19.989,8.773 23.227,12.011 23.227,16C23.227,19.989 19.989,23.227 16,23.227C12.011,23.227 8.773,19.989 8.773,16C8.773,12.011 12.011,8.773 16,8.773Z"/>
</g>
<g>
<circle class="toggle-circle" cx="16" cy="16" r="5.511"/>
</g>
</svg>
</button>
@media (prefers-color-sheme: light) {
:root {
--background: white;
--color: hsl(0, 15%, 87%);
}
}

.dark-theme {
--background: hsl(336, 8%, 12%);
--color: hsl(0, 15%, 87%);
}

.light-theme {
--background: white;
--color: hsl(0, 15%, 0%);
}
@media (prefers-color-sheme: light) {
:root {
--background: white;
--color: hsl(0, 15%, 87%);
}
}

.dark-theme {
--background: hsl(336, 8%, 12%);
--color: hsl(0, 15%, 87%);
}

.light-theme {
--background: white;
--color: hsl(0, 15%, 0%);
}
Posting this in parts now as I'm limited as to how much I can post in one message.
#theme-toggle {
position: relative;
cursor: pointer;
background: 0;
border: 0;
opacity: .7;
padding: .45rem;
border-radius: var(--br-circ);
isolation: isolate;
margin-left: 50%;
}

#theme-toggle svg {
fill: var(--color);
}

#theme-toggle::before {
content: '';
position: absolute;
inset: 0;
background: hsla(0, 0%, 50%, .25);
border-radius: inherit;
transform: scale(0);
opacity: 0;
z-index: -1;
}

.dark-theme #theme-toggle::before {
animation: pulseToDark 650ms ease-out;
}

.dark-theme #theme-toggle::before {
animation: pulseToLight 650ms ease-out;
}

#theme-toggle::after {
content: attr(aria-label);
position: absolute;
color: var(--color);
background: var(--background);
width: max-content;
font-size: var(--fs--1);
left: -100%;
top: 100%;
margin: 0 auto;
padding: .2em .3em;
border-radius: var(--br-mid);
opacity: 0;
transform: scale(0);
transform-origin: top;
transition: transform 0ms linear 100ms, opacity 100ms linear;
}
#theme-toggle {
position: relative;
cursor: pointer;
background: 0;
border: 0;
opacity: .7;
padding: .45rem;
border-radius: var(--br-circ);
isolation: isolate;
margin-left: 50%;
}

#theme-toggle svg {
fill: var(--color);
}

#theme-toggle::before {
content: '';
position: absolute;
inset: 0;
background: hsla(0, 0%, 50%, .25);
border-radius: inherit;
transform: scale(0);
opacity: 0;
z-index: -1;
}

.dark-theme #theme-toggle::before {
animation: pulseToDark 650ms ease-out;
}

.dark-theme #theme-toggle::before {
animation: pulseToLight 650ms ease-out;
}

#theme-toggle::after {
content: attr(aria-label);
position: absolute;
color: var(--color);
background: var(--background);
width: max-content;
font-size: var(--fs--1);
left: -100%;
top: 100%;
margin: 0 auto;
padding: .2em .3em;
border-radius: var(--br-mid);
opacity: 0;
transform: scale(0);
transform-origin: top;
transition: transform 0ms linear 100ms, opacity 100ms linear;
}
#theme-toggle:hover,
#theme-toggle:focus {
outline: 0;
opacity: 1;
background: hsla(0, 0%, 50%, .15);
}

#theme-toggle:hover::after,
#theme-toggle:focus-visible::after {
opacity: .7;
transform: scale(1);
transition: transform 50ms linear, opacity 50ms linear;
}

.toggle-circle {
transition: transform 500ms ease-out;
}

.light-theme .toggle-circle {
transform: translateX(-10%);
}

.toggle-sun {
transform-origin: 50%;
transition: transform 750ms cubic-bezier(0,0,.35,1.5);
}

.light-theme .toggle-sun {
transform: rotate(.5turn);
}

@keyframes pulseToDark {
0% {
transform: scale(0);
opacity: .5;
}
10% {
transform: scale(1);
}
50% {
opacity: 1;
}
100% {
transform: scale(1);
opacity: 0;
}
}

@keyframes pulseToLight {
0% {
transform: scale(0);
opacity: .5;
}
10% {
transform: scale(1);
}
50% {
opacity: 1;
}
100% {
transform: scale(1);
opacity: 0;
}
}
#theme-toggle:hover,
#theme-toggle:focus {
outline: 0;
opacity: 1;
background: hsla(0, 0%, 50%, .15);
}

#theme-toggle:hover::after,
#theme-toggle:focus-visible::after {
opacity: .7;
transform: scale(1);
transition: transform 50ms linear, opacity 50ms linear;
}

.toggle-circle {
transition: transform 500ms ease-out;
}

.light-theme .toggle-circle {
transform: translateX(-10%);
}

.toggle-sun {
transform-origin: 50%;
transition: transform 750ms cubic-bezier(0,0,.35,1.5);
}

.light-theme .toggle-sun {
transform: rotate(.5turn);
}

@keyframes pulseToDark {
0% {
transform: scale(0);
opacity: .5;
}
10% {
transform: scale(1);
}
50% {
opacity: 1;
}
100% {
transform: scale(1);
opacity: 0;
}
}

@keyframes pulseToLight {
0% {
transform: scale(0);
opacity: .5;
}
10% {
transform: scale(1);
}
50% {
opacity: 1;
}
100% {
transform: scale(1);
opacity: 0;
}
}
And this is a simple meta tag I thought might be causing my issue which I had set in GO's head.html in layout/partials
<meta name="color-scheme" content="dark light">
<meta name="color-scheme" content="dark light">
And last the JS file
const themeToggle = document.querySelector('#theme-toggle');

themeToggle.addEventListener('click', () => {
document.body.classList.contains("dark-theme")
? enableLightMode()
: enableDarkMode();
})

function enableLightMode() {
document.body.classList.remove("dark-theme");
document.body.classList.add("light-theme");
themeToggle.setAttribute("aria-label", "Switch to dark theme");
}

function enableDarkMode() {
document.body.classList.remove("light-theme");
document.body.classList.add("dark-theme");
themeToggle.setAttribute("aria-label", "Switch to light theme");
}

function setThemePref() {
if (window.matchMedia('(prefers-color-sheme: light)').matches) {
enableLightMode();
return;
}
enableDarkMode();
}

document.onload = setThemePref();
const themeToggle = document.querySelector('#theme-toggle');

themeToggle.addEventListener('click', () => {
document.body.classList.contains("dark-theme")
? enableLightMode()
: enableDarkMode();
})

function enableLightMode() {
document.body.classList.remove("dark-theme");
document.body.classList.add("light-theme");
themeToggle.setAttribute("aria-label", "Switch to dark theme");
}

function enableDarkMode() {
document.body.classList.remove("light-theme");
document.body.classList.add("dark-theme");
themeToggle.setAttribute("aria-label", "Switch to light theme");
}

function setThemePref() {
if (window.matchMedia('(prefers-color-sheme: light)').matches) {
enableLightMode();
return;
}
enableDarkMode();
}

document.onload = setThemePref();
I think this covers it
Jochem
Jochem•17mo ago
I think
document.onload = setThemePref();
document.onload = setThemePref();
should be
document.onload = setThemePref;
document.onload = setThemePref;
Peacock Plume
Peacock Plume•17mo ago
Aha I see, I'm very novice in JS, wouldn't be able to tell. Anyway I switched this now over and that's not fixing it tho. Wonder if it has something to do with me being on localhost instead of on a server
Jochem
Jochem•17mo ago
hm, well, none of this code wouldn't necessarily remember your previous setting, it would always default to whatever your OS or browser preference is
Peacock Plume
Peacock Plume•17mo ago
So you think this might be it and it actually works, but only once I would actually publish the site to a server?
Jochem
Jochem•17mo ago
no, there's nothing in this code that stores the setting beyond the current page load every time the page loads, it will run setThemePref, which will then check if the browser / OS prefers light or dark, and sets the theme appropriately
Peacock Plume
Peacock Plume•17mo ago
I see, weird did I miss something in the video then?!
Jochem
Jochem•17mo ago
I probably watched it a while ago, but I don't remember if storing the preference is part of the video
Peacock Plume
Peacock Plume•17mo ago
Possible, however his does work, so either he left that out or I must have missed a part. 🤔
Jochem
Jochem•17mo ago
is it that it doesn't work on reload, or does toggling not work either?
Peacock Plume
Peacock Plume•17mo ago
No, when refreshing the page it simply switches to dark theme, and everything else seems to work normal, or fine.
Jochem
Jochem•17mo ago
so clicking the toggle switches back and forth between light and dark, but refreshing switches back to dark regardless of what it was set to before?
Peacock Plume
Peacock Plume•17mo ago
Yes!
Jochem
Jochem•17mo ago
ok, that's exactly what the code you shared should be doing
Peacock Plume
Peacock Plume•17mo ago
So basically all I need is a way to store the user settings via JS I presume?
Jochem
Jochem•17mo ago
store and retrieve on load, yes window.localStorage is probably the easiest way to store data in the user's browser: https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage
Peacock Plume
Peacock Plume•17mo ago
Thanks a lot for your time, I'll look for a tutorial on how to do this correctly and try implement it.
Jochem
Jochem•17mo ago
No worries, always glad to help 🙂 This is untested, but it should work, spoilered in case you want to figure it out yourself
function enableLightMode() {
localStorage.setItem("theme", "light"); //added
document.body.classList.remove("dark-theme");
document.body.classList.add("light-theme");
themeToggle.setAttribute("aria-label", "Switch to dark theme");
}

function enableDarkMode() {
localStorage.setItem("theme", "dark"); //added
document.body.classList.remove("light-theme");
document.body.classList.add("dark-theme");
themeToggle.setAttribute("aria-label", "Switch to light theme");
}

function setThemePref() {
/*added from here*/
const theme = localStorage.getItem("theme");
if (theme) {
if (theme === "light") {
enableLightMod();
return;
}

enableDarkMode();
return;
}
/* to here */

if (window.matchMedia('(prefers-color-sheme: light)').matches) {
enableLightMode();
return;
}

enableDarkMode();
}
function enableLightMode() {
localStorage.setItem("theme", "light"); //added
document.body.classList.remove("dark-theme");
document.body.classList.add("light-theme");
themeToggle.setAttribute("aria-label", "Switch to dark theme");
}

function enableDarkMode() {
localStorage.setItem("theme", "dark"); //added
document.body.classList.remove("light-theme");
document.body.classList.add("dark-theme");
themeToggle.setAttribute("aria-label", "Switch to light theme");
}

function setThemePref() {
/*added from here*/
const theme = localStorage.getItem("theme");
if (theme) {
if (theme === "light") {
enableLightMod();
return;
}

enableDarkMode();
return;
}
/* to here */

if (window.matchMedia('(prefers-color-sheme: light)').matches) {
enableLightMode();
return;
}

enableDarkMode();
}
Peacock Plume
Peacock Plume•17mo ago
Thanks a lot, yeah I'm watching a tutorial on it, so I can learn something, but this will help in case I can't figure it out in the end. Alright so I did watch a tutorial about it but was still kind of lost so I gave your code a try... First off you had a typo up there,
if (theme === "light") {
enableLightMod(); // should be enableLightMode();
return;
}
if (theme === "light") {
enableLightMod(); // should be enableLightMode();
return;
}
And then I found out that if I actually set this back to what I had before, with the parentheses it would work, but without it wouldn't.
document.onload = setThemePref();
document.onload = setThemePref();
So thanks a lot not it actually works and retrieves the data from local storage.
Jochem
Jochem•17mo ago
Glad that it's working now, and nice catch on the typo! That's a bit odd that the setThemePref() works. What's supposed to be happening there, is that you're assigning a callback function to the onload property of document, which is then supposed to be called when the document loads the DOM (excluding transferring images). Whenever you do something like thatthat, you have to pass in the function, without calling the function. What's happening now, is that document.load is getting set to the result of calling setThemePref(), which could potentially run before the DOM is loaded, and therefor before any of the setBlankMode function calls would work. I'm guessing you're either using codepen, including the script tag at the bottom of the body tag, or using defer in the tag itself? Either way it should work the same as just calling setThemePref() without document.onload = in front of it
Peacock Plume
Peacock Plume•17mo ago
Alright, thanks for the insight! Yeah it's odd, but it works. I set this to solved as my issue is kinda solved. This is just a personal for fun site anyway, for practicing and trying out things. Edit: And to answer the question you posed, no there's no codepen involved, I'm only working offline on localhost, hugo environment and this is my <script src="/js/theme-toggle.js"></script> script located in a script-footer.html "golang html file". And I confirm, yes indeed setThemePref() without document.onload = equally works! I found another typo that I made this time prefs-color-sheme should be prefs-color-scheme. Just to correct my mistake. And I also had a look into what you meant by defer, so yeah you could see that I was on normal loading… I didn't know about normal, defer and async loading yet, good to know, thanks for making me look into that! One thing I wanted to say about the video from Kevin. I checked the animation again from google fonts and very early in the video Kevin mentions that his toggle-circle or rather the moon looked a little different on google fonts, and I think the main reason is that their toggle-circle seems to not only simply move left but also slightly grow or scale at the very end of the animation so it's barely noticeable. I tried to recreate this, but could not solve this. Seems to be quite a challenge to get scale to work with translate at the same time. Anyway something I think Kevin didn't notice in the google font animation as he didn't implement this effect. Just something I wanted to add. In the end it was super easy to do the growing effect or scale by doing this instead of trying to animate scale, now it really looks like a moon with the slightly bigger size.
/* Simply used "guess" radius is what 'r' stands for in the svg code */
.light-theme .toggle-circle {
transform: translateX(-10%);
r: 6.1;
}
/* Simply used "guess" radius is what 'r' stands for in the svg code */
.light-theme .toggle-circle {
transform: translateX(-10%);
r: 6.1;
}