Tailwind - Dark Mode
Installing Tailwind CSS with Gatsby - Part 2
...or Dark Mode in Paradise!
I did a lot of research for implementing Dark Mode and I wanted to do it with Tailwind CSS. Tailwind has its own implementation. I originally had the plan of implementing it like this. However, after researching how most developers were doing this, I saw benefit in using CSS Variables. All of this was new to me. I've never been a web designer and I haven't fully grokked CSS. See the Acknowledgements below for all the sites that gave me clues and information!
I ended up working from Josh W. Comeau's "The Quest for the Perfect Dark Mode"
Turn on the dark in Tailwind Config
In tailwind.config.js
the default for darkMode
is false. My plan was to set a class of either light or dark as high up on the site as I could. So my first step was to set darkMode to class
.
1module.exports = {2 purge: ['./src/**/*.{js,jsx,ts,tsx}'],3 darkMode: 'class', // or 'media' or 'class'4 theme: {5 extend: {},6 },7 variants: {},8 plugins: [],9}
Develop the pre-Gatsby script
As Josh pointed out in his post, dark mode truly was nasty tough to do in Gatsby. The script needs to run before Gatsby starts hydrating. His goal was the same as mine which was helpful! I wanted to know what the user wanted before the home page displayed.
What the user wanted was actually defined by Josh, and I agreed:
- user can click a toggle to switch
- user's preference should be saved
- color scheme should default to OS setting
- if all else fails, go with light mode
Josh had a couple of other requirements that I didn't think of or realize would be an issue:
- no flicker on first load ⁉
- site should never show the wrong toggle state (well, yes, of course...why would it?)
I followed Josh's steps, creating a ThemeContext and learning about React Context a bit.
His function was called getInitialColorMode
while mine is getInitialTheme
. They both do the same thing in the long run but since Tailwind was going to handle the colors, I only had to set the theme as light
or dark
and set a class on the body
element.
1import * as React from 'react'2
3const MagicScriptTag = () => {4 const codeToRunOnClient = `5 (function() {6 function getInitialTheme() {7 const persistedColorPreference = window.localStorage.getItem('color-mode')8 const hasPersistedPreference = typeof persistedColorPreference === 'string'9 // If the user has explicitly chosen light or dark,10 // let's use it. Otherwise, this value will be null.11 if (hasPersistedPreference) {12 return persistedColorPreference13 }14 // If they haven't been explicit, let's check the media15 // query16 const mql = window.matchMedia('(prefers-color-scheme: dark)')17 const hasMediaQueryPreference = typeof mql.matches === 'boolean'18 if (hasMediaQueryPreference) {19 return mql.matches ? 'dark' : 'light'20 }21 // If they are using a browser/OS that doesn't support22 // color themes, let's default to 'light'.23 return 'light'24 }25
26 const theme = getInitialTheme()27
28 const body = document.body29
30 body.className = theme31
32 const root = document.documentElement;33 root.style.setProperty('--initial-theme', theme);34 })()`35
36 // eslint-disable-next-line react/no-danger37 return <script dangerouslySetInnerHTML={{ __html: codeToRunOnClient }} />38}39export const onRenderBody = ({ setPreBodyComponents }) => {40 setPreBodyComponents(<MagicScriptTag />)41}
Dark Mode Toggle component
I went extremely simple on the toggle component. I decided to just use 🌞 and 🌙 emojis. Some of the sites I looked at had much better looking ideas and perhaps someday mine will too. The DarkToggle component uses theme context so it can access the theme and set it when it updates. I almost understood it actually! I added that to my header component.
Color scheme
Since there will be a light or dark class added to body, I can use CSS Variables to create .light
and .dark
styles that will live at the root and be available everywhere. I created Tailwind theme colors using those CSS variable names.
After that, dark mode worked!
I thought since I'm going with teal for my primary color, perhaps the blue-gray Tailwind color pallette would do well instead of the standard gray. I ended up using hex codes initially instead of Tailwind codes, however, and at the current state of the project (here on 2021-01-14) I'm still not using Tailwind codes.
The Recipe
set
darkMode = 'class'
intailwind.config.js
create Theme Provider component
1import * as React from 'react'2import { useState, useEffect } from 'react'3
4export const ThemeContext = React.createContext()5
6export const ThemeProvider = ({ children }) => {7 const [theme, rawSetTheme] = useState(undefined)8 useEffect(() => {9 const root = window.document.documentElement10 const initialTheme = root.style.getPropertyValue('--initial-theme')11 rawSetTheme(initialTheme)12 }, [])13
14 const setTheme = (value) => {15 rawSetTheme(value)16
17 window.localStorage.setItem('theme', value)18 window.document.documentElement.className = value19 }20
21 return <ThemeContext.Provider value={{ theme, setTheme }}>{children}</ThemeContext.Provider>22}
- create App component
1// Your App.js may live elsewhere of course. I haven't decided it should be as of now!2import * as React from 'react'3import { ThemeProvider } from './ThemeContext'4
5const App = ({ children }) => {6 return <ThemeProvider>{children}</ThemeProvider>7}8
9export default App
- add App and Tailwind CSS to
gatsby-browser.js
1import * as React from 'react'2import App from './src/components/App'3import './src/styles/global.css'4
5export const wrapRootElement = ({ element }) => {6 return <App>{element}</App>7}
- create
gatsby-ssr.js
1import * as React from 'react'2import App from './src/components/App'3
4const MagicScriptTag = () => {5 const codeToRunOnClient = `6 (function() {7 function getInitialTheme() {8 const persistedColorPreference = window.localStorage.getItem('theme')9 const hasPersistedPreference = typeof persistedColorPreference === 'string'10 // If the user has explicitly chosen light or dark,11 // let's use it. Otherwise, this value will be null.12 if (hasPersistedPreference) {13 return persistedColorPreference14 }15 // If they haven't been explicit, let's check the media16 // query17 const mql = window.matchMedia('(prefers-color-scheme: dark)')18 const hasMediaQueryPreference = typeof mql.matches === 'boolean'19 if (hasMediaQueryPreference) {20 return mql.matches ? 'dark' : 'light'21 }22 // If they are using a browser/OS that doesn't support23 // color themes, let's default to 'light'.24 return 'light'25 }26
27 const theme = getInitialTheme()28
29 const root = document.documentElement30 root.className = theme31 root.style.setProperty('--initial-theme', theme)32 })()`33
34 // eslint-disable-next-line react/no-danger35 return <script dangerouslySetInnerHTML={{ __html: codeToRunOnClient }} />36}37export const onRenderBody = ({ setPreBodyComponents }) => {38 setPreBodyComponents(<MagicScriptTag />)39}40
41export const wrapPageElement = ({ element }) => {42 return <App>{element}</App>43}
- create DarkToggle component
1import * as React from 'react'2import { useContext } from 'react'3
4import { ThemeContext } from './ThemeContext'5
6const DarkToggle = () => {7 const { theme, setTheme } = useContext(ThemeContext)8
9 if (!theme) {10 return null11 }12
13 return (14 <button15 type='button'16 onClick={() => {17 setTheme(theme === 'light' ? 'dark' : 'light')18 }}19 className='block'20 >21 {`${theme === 'light' ? '🌙' : '🌞'}`}22 </button>23 )24}25
26export default DarkToggle
- add DarkToggle to Header Component and add some Tailwind to test it out.
1import { Link } from 'gatsby'2import PropTypes from 'prop-types'3import * as React from 'react'4
5import DarkToggle from './DarkToggle'6
7const Header = ({ siteTitle }) => (8 <header className='bg-gray-300 text-gray-900 dark:bg-gray-900 dark:text-gray-200 p-4 flex justify-between items-center'>9 <h1 className='text-gray-900 dark:text-gray-200 no-underline m-0'>10 <Link to='/'>{siteTitle}</Link>11 </h1>12 <DarkToggle />13 </header>14)15
16Header.propTypes = {17 siteTitle: PropTypes.string,18}19
20Header.defaultProps = {21 siteTitle: ``,22}23
24export default Header
- add color scheme to Global CSS
1@tailwind base;2@tailwind components;3@tailwind utilities;4
5.light {6 --primary: #007889;7 --background: #e5e7eb;8 --surface: #ffffff;9 --container: #d1d5db;10 --branded-surface: #d1d5db;11 --text: #1f2937;12 --text-header: #09090a;13 --link: #007889;14 --muted: #4b5563;15 --text-disabled: #6b7280;16}17
18.dark {19 --primary: #f4901e;20 --background: #1f2937;21 --surface: #000000;22 --container: #111827;23 --branded-surface: #111a1c;24 --text: #e5e7eb;25 --text-header: #f3f4f6;26 --link: #f4901e;27 --muted: #9ca3af;28 --text-disabled: #6b7280;29}
- add color scheme to Tailwind CSS
1module.exports = {2 purge: ['./src/**/*.{js,jsx,ts,tsx}'],3 darkMode: 'class', // or 'media' or 'class'4 theme: {5 extend: {6 colors: {7 primary: 'var(--primary)',8 background: 'var(--background)',9 surface: 'var(--surface)',10 container: 'var(--container)',11 brandedSurface: 'var(--branded-surface)',12 body: 'var(--text)',13 header: 'var(--text-header)',14 link: 'var(--link)',15 muted: 'var(--muted)',16 disabled: 'var(--text-disabled)',17 },18 },19 },20 variants: {21 extend: {},22 },23 plugins: [],24}
At this point, my recipe is at an end. There were a few nice extra ideas in Josh's post but I decided against them, for now. I tried implementing the minification process he describes using Terser but I had too many problems for too little joy. I also decided to skip the situation when there is no JavaScript.
Acknowledgements
How To Add Dark Mode To Your Gridsome Blog With Tailwind CSS
A Simple Guide to Dark Mode with Gatsby.js
Sharks win 4-3 in Shootout against the Coyotes
Attributions
Photo by Ken Feliciano (Me!)