Toggling themes with TailwindCSS
Posted on
In my previous post, I went over adding TailwindCSS to a new Eleventy project. I found that after adding TailwindCSS, the default styles leave a lot to be desired. However I do think this is a good thing though in that Tailwind doesn't force any styles on you, yet I don't want to spend a lot of time on setting up the base styles. I ended up installing their typography plugin npm i @tailwindcss/typography
to help with this matter. By adding a prose
class around content, I'll gain some basic styles instantly. To get it working, I just had to add require('@tailwindcss/typography')
to the plugins
array of the tailwind.config.js
.
The approach I'm going to take to toggle themes relies on CSS variables and a data-
attribute on the <body>
element. This approach also scales to as many themes as you need. I'll declare my themes' colors as css variables as well as create variables for things that will change such as the background color and the text color. Themes will then create classes based off of these variables.
:root {
--imperial-red: #e63946;
--honeydew: #f1faee;
--powder-blue: #a8dadc;
--celadon-blue: #457b9d;
--prussian-blue: #1d3557;
--theme--color__link: var(--imperial-red);
/* set dark theme as default */
--theme--color__background: var(--prussian-blue);
--theme--color__text: var(--honeydew);
--theme--color__border: var(--powder-blue);
}
body[data-theme='dark'] {
--theme--color__background: var(--prussian-blue);
--theme--color__text: var(--honeydew);
--theme--color__border: var(--powder-blue);
}
body[data-theme='light'] {
--theme--color__background: var(--honeydew);
--theme--color__text: var(--prussian-blue);
--theme--color__border: var(--celadon-blue);
}
Since postcss-import
doesn't want anything above @import
statements, and I need Tailwind to be cognizant of these variables, I put this in a themes.css
file and import that before importing Tailwind in tailwind.css
. I can then create classes by extending Tailwind with custom classes in my tailwind.config.js
. The changes below will generate classes like text-link
and bg-theme
.
theme: {
extend: {
colors: {
theme: 'var(--theme--color__text)',
link: 'var(--theme--color__link)',
background: 'var(--theme--color__background)',
border: 'var(--theme--color__border)'
},
textColor: {
theme: 'var(--theme--color__text)',
link: 'var(--theme--color__link)'
},
backgroundColor: {
theme: 'var(--theme--color__background)',
link: 'var(--theme--color__link)'
},
borderColor: {
theme: 'var(--theme--color__border)',
link: 'var(--theme--color__link)'
}
}
}
Since I am also using the typography plugin, I have to override their colors with my theme colors in tailwind.config.js
as well.
theme: {
extend: {...},
typography: {
default: {
css: {
color: 'var(--theme--color__text)',
a: {
color: 'var(--theme--color__link)'
},
button: {
color: 'var(--theme--color__link)'
},
strong: {
color: 'var(--theme--color__link)'
},
hr: {
borderColor: 'var(--theme--color__border)'
},
h1: {
color: 'var(--theme--color__text)'
},
h2: {
color: 'var(--theme--color__text)'
},
h3: {
color: 'var(--theme--color__text)'
},
h4: {
color: 'var(--theme--color__text)'
},
h5: {
color: 'var(--theme--color__text)'
},
h6: {
color: 'var(--theme--color__text)'
},
'ol li:before': {
color: 'var(--theme--color__border)'
},
'ul li:before': {
backgroundColor: 'var(--theme--color__border)'
},
blockquote: {
borderLeftColor: 'var(--theme--color__border)',
color: 'var(--theme--color__text)'
},
pre: {
color: 'var(--theme--color__background)',
backgroundColor: 'var(--theme--color__text)'
},
code: {
color: 'var(--theme--color__link)'
},
thead: {
color: 'var(--theme--color__link)'
}
}
}
}
}
Now all I need is a way to toggle the themes. Inside of my header I added a button with an id to select it and a data-
attribute to know which theme to switch to. I'll use an event handler to listen to clicks on the button and change the <body>
's data-theme
attribute. In addition, I'll save the theme choice to localStorage
with a dajo.theme
variable to set the theme choice on page load to keep the theme consistent as users navigate through different pages. I also tried to compensate for a user's preference of dark mode. In the end, I put the following in a src/scripts/index.js
file and loaded that in my default layout.
const STORAGE_ITEM = 'dajo.theme'
const DARK_MODE = 'dark'
const LIGHT_MODE = 'light'
const theme = window && window.localStorage.getItem(STORAGE_ITEM)
const userPrefersDarkMode =
window &&
window.matchMedia &&
window.matchMedia('(prefers-color-scheme: dark)').matches
const themeToggleElt = document && document.getElementById('toggle-theme')
const themeApplyElt = document && document.body
function setTheme(currentTheme, nextTheme) {
themeApplyElt.dataset.theme = currentTheme
themeToggleElt.src = themeToggleElt.src.replace(currentTheme, nextTheme)
themeToggleElt.alt = themeToggleElt.alt.replace(currentTheme, nextTheme)
themeToggleElt.nextElementSibling.innerText = themeToggleElt.nextElementSibling.innerText.replace(
currentTheme,
nextTheme
)
themeToggleElt.dataset.themeSwitcher = nextTheme
}
// Set theme on page load
if (theme) {
if (theme === DARK_MODE) {
setTheme(DARK_MODE, LIGHT_MODE)
} else if (theme === LIGHT_MODE) {
setTheme(LIGHT_MODE, DARK_MODE)
}
} else {
setTheme(
userPrefersDarkMode ? DARK_MODE : LIGHT_MODE,
userPrefersDarkMode ? LIGHT_MODE : DARK_MODE
)
}
// Toggle theme on click
themeToggleElt &&
themeToggleElt.addEventListener('click', (event) => {
const oldTheme = themeApplyElt.dataset.theme
const newTheme = event.target.dataset.themeSwitcher
window.localStorage.setItem(STORAGE_ITEM, newTheme)
setTheme(newTheme, oldTheme)
})
Toggling the button content doesn't scale to multiple themes but everything else about this approach will work for three or more themes.