Static HTML/CSS design which served as a basis for my implementation uses CSS variables throughout. I consider my UI/UX and CSS/HTML skills to be junior; I didn’t want making a huge mess of it by refactoring it. Therefore, I’ve adapted it in NextJS with as little changes as I could. Dark colors looks good to my non-designer eyes, but because this website is a learning opportunity I fancied implementing theme switching.
After doing a quick research I found a few implementation approaches. The most appropriate solution from architectural standpoint is to use native’s React context hook (ie. React.createContex()
) and implement ThemeContext containing custom themes but this approach would’ve entailed significantly more changes to a project which I was comfortable with at the time. I didn’t want to spend more than a day on this functionality.
The second approach which I investigated was based on standard CSS variables and involved customization of root [_document.jsx]
file. Even though it seems to be close to my situation I didn’t like the customizations required for root's [_document.jsx]
. It looked very “hacky”.
And then I came across a blog post by Rob Morieson, which seemed to cover the main points for me: no need to refactor CCS, no significant application architecture refactoring and it was something that could've been implemented in 30 minutes and that's why it became an avenue for my solution.
This is how a snippet of global.css
file containing main theming variables looks:
global.css
/* dark theme */
body[data-theme="dark"] {
--light-color: #fff;
--light-color-alt: #afb6cd;
--primary-background-color: #131417;
--secondary-background-color: #252830;
--transparent-light-color: rgba(255,255,255,.05);
--transparent-dark-color: rgba(0,0,0,.75);
--hover-light-color: var(--light-color);
--hover-dark-color: var(--primary-background-color);
}
/* Theme color change */
body[data-theme="light"]{
--light-color: #3d3d3d;
--light-color-alt: rgba(0,0,0,.6);
--primary-background-color: #fff;
--secondary-background-color: #f1f1f1;
--transparent-dark-color: var(--secondary-background-color);
--transparent-light-color: rgba(0,0,0,.1);
--hover-light-color: #fff;
}
This code snippet below covers basically all of the logic responsible for changing and persisting theme changes in local storage of a browser.
const toggleTheme = () => {
if(activeTheme == "light") {
setActiveTheme("dark");
} else {
setActiveTheme("light");
}
}
const [toggleMenu, setToggleMenu] = useState(false);
const [activeTheme, setActiveTheme] = useState();
useEffect(() => {
const savedTheme = localStorage.getItem("theme");
if( savedTheme && savedTheme !== undefined ) setActiveTheme(savedTheme);
}, []);
useEffect(() => {
document.body.dataset.theme = activeTheme;
window.localStorage.setItem("theme", activeTheme);
}, [activeTheme]);
-----------
<button className="btn place-items-center" id="theme-toggle-btn" onClick={toggleTheme}>
<i className="ri-sun-fill sun-icon"></i>
<i className="ri-sun-line moon-icon"></i>
</button>
Two useEffects()
are used with different dependencies: one of them is invoked only once, resulting in setting activeTheme
value from local storage if it was persisted prior and the second useEffect()
uses that value as a dependency which will be checked during each render of the components.
It's hard to say how much better or worse this approach compared to ThemeContext/ThemeProvider
implementation, but I haven't noticed any performance issues during testing.
The only note I should make: while in DEV mode theme value was overridden in local storage on page refreshes, but in production mode this works properly. I don’t have a definitive answer about it, but development mode in NextJS deviates significantly from how production versions of site operates.