{"id":296088,"date":"2019-09-25T07:10:14","date_gmt":"2019-09-25T14:10:14","guid":{"rendered":"https:\/\/css-tricks.com\/?p=296088"},"modified":"2019-09-25T07:10:14","modified_gmt":"2019-09-25T14:10:14","slug":"a-dark-mode-toggle-with-react-and-themeprovider","status":"publish","type":"post","link":"https:\/\/css-tricks.com\/a-dark-mode-toggle-with-react-and-themeprovider\/","title":{"rendered":"A Dark Mode Toggle with React and ThemeProvider"},"content":{"rendered":"
I like when websites have a dark mode option. Dark mode<\/a> makes web pages easier for me to read and helps my eyes feel more relaxed. Many websites, including YouTube and Twitter, have implemented it already, and we\u2019re starting to see it trickle onto many other sites as well.<\/p>\n In this tutorial, we\u2019re going to build a toggle that allows users to switch between light and dark modes, using a If that sounds hard, I promise it\u2019s not! Let\u2019s dig in and make it happen.<\/p>\n <\/p>\n \n See the Pen We\u2019ll use create-react-app<\/a> to initiate a new project:<\/p>\n Next, open a separate terminal window and install styled-components:<\/p>\n Next thing to do is create two files. The first is Feel free to customize variables any way you want, because this code is used just for demonstration purposes. <\/p>\n Go to the App.js file. We\u2019re going to delete everything in there and add the layout for our app. Here\u2019s what I did:<\/p>\n This imports our light and dark themes. The Here\u2019s roughly what we have so far:<\/p>\n There is no magic switching between themes yet, so let\u2019s implement toggling functionality. We are only going to need a couple lines of code to make it work.<\/p>\n First, import the Next, use the hook to create a local state which will keep track of the current theme and add a function to switch between themes on click: <\/p>\n After that, all that\u2019s left is to pass this function to our button element and conditionally change the theme. Take a look: <\/p>\n Earlier in our We\u2019re generally done here because you now know how to create toggling functionality. However, we can always do better, so let\u2019s improve the app by creating a custom We\u2019ll keep everything inside one file for simplicity\u2019s sake,, so let\u2019s create a new one called You can download icons from here<\/a> and here<\/a>. Also, if we want to use icons as components, remember about importing them as React components<\/a>.<\/p>\n We passed two props inside: the We\u2019ve also imported a Importing icons as components allows us to directly change the styles of the SVG icons. We\u2019re checking if the Don\u2019t forget to replace the button with the The last thing to do is import our component inside App.js and pass required props to it. Also, to add a bit more interactivity, I\u2019ve passed condition to toggle between “light” and \u201cdark” in the heading when the theme changes:<\/p>\n Don\u2019t forget to credit the flaticon.com<\/a> authors for the providing the icons.<\/p>\n Now that\u2019s better: <\/p>\n While building an application, we should keep in mind that the app must be scalable, meaning, reusable, so we can use in it many places, or even different projects.<\/p>\n That is why it would be great if we move our toggle functionality to a separate place \u2014 so, why not to create a dedicated account hook for that?<\/p>\n Let\u2019s create a new file called useDarkMode.js in the project We\u2019ve added a couple of things here. We want our theme to persist between sessions in the browser, so if someone has chosen a dark theme, that\u2019s what they\u2019ll get on the next visit to the app. That\u2019s a huge UX<\/abbr> improvement. For this reasons we use We\u2019ve also implemented the Now, let\u2019s implement the This almost<\/em> works almost perfectly, but there is one small thing we can do to make our experience better. Switch to dark theme and reload the page. Do you see that the sun icon loads before the moon icon for a brief moment?<\/p>\n That happens because our So far, I found two solutions. The first is to check if there is a value in However, I am not sure if it\u2019s a good practice to do checks like that inside This one will be a bit more complicated. We will create another state and call it You might have noticed that we\u2019ve got some pieces of code that are repeated. We always try to follow the DRY<\/a> principle while writing the code, and right here we\u2019ve got a chance to use it. We can create a separate function that will set our state and pass With this function in place, we can refactor our useDarkMode.js a little:<\/p>\n We\u2019ve only changed code a little, but it looks so much better and is easier to read and understand! <\/p>\n Getting back to If it hasn\u2019t happened yet, we will render an empty div:<\/p>\n Here is how complete code for the App.js: <\/p>\n This part is not required, but it will let you achieve even better user experience. This media feature is used to detect if the user has requested the page to use a light or dark color theme based on the settings in their OS<\/abbr>. For example, if a user\u2019s default color scheme on a phone or laptop is set to dark, your website will change its color scheme accordingly to it. It\u2019s worth noting that this media query is still a work in progress and is included in the Media Queries Level 5 specification<\/a>, which is in Editor\u2019s Draft.<\/p>\n<ThemeProvider<\/a><\/code> wrapper from the styled-components<\/a> library. We\u2019ll create a
useDarkMode<\/code> custom hook, which supports the
prefers-color-scheme<\/a><\/code> media query to set the mode according to the user\u2019s OS<\/abbr> color scheme settings.<\/p>\n
\n Day\/night mode switch toggle with React and ThemeProvider<\/a> by Maks Akymenko (@maximakymenko<\/a>)
\n on CodePen<\/a>.<\/span>\n<\/p>\nLet\u2019s set things up<\/h3>\n
npx create-react-app my-app\r\ncd my-app\r\nyarn start<\/code><\/pre>\n
yarn add styled-components<\/code><\/pre>\n
global.js<\/code>, which will contain our base styling, and the second is
theme.js<\/code>, which will include variables for our dark and light themes:<\/p>\n
\/\/ theme.js\r\nexport const lightTheme = {\r\n body: '#E2E2E2',\r\n text: '#363537',\r\n toggleBorder: '#FFF',\r\n gradient: 'linear-gradient(#39598A, #79D7ED)',\r\n}\r\n\r\nexport const darkTheme = {\r\n body: '#363537',\r\n text: '#FAFAFA',\r\n toggleBorder: '#6B8096',\r\n gradient: 'linear-gradient(#091236, #1E215D)',\r\n}<\/code><\/pre>\n
\/\/ global.js\r\n\/\/ Source: https:\/\/github.com\/maximakymenko\/react-day-night-toggle-app\/blob\/master\/src\/global.js#L23-L41\r\n\r\nimport { createGlobalStyle } from 'styled-components';\r\n\r\nexport const GlobalStyles = createGlobalStyle`\r\n *,\r\n *::after,\r\n *::before {\r\n box-sizing: border-box;\r\n }\r\n\r\n body {\r\n align-items: center;\r\n background: ${({ theme }) => theme.body};\r\n color: ${({ theme }) => theme.text};\r\n display: flex;\r\n flex-direction: column;\r\n justify-content: center;\r\n height: 100vh;\r\n margin: 0;\r\n padding: 0;\r\n font-family: BlinkMacSystemFont, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;\r\n transition: all 0.25s linear;\r\n }<\/code><\/pre>\n
import React from 'react';\r\nimport { ThemeProvider } from 'styled-components';\r\nimport { lightTheme, darkTheme } from '.\/theme';\r\nimport { GlobalStyles } from '.\/global';\r\n\r\nfunction App() {\r\n return (\r\n <ThemeProvider theme={lightTheme}>\r\n <>\r\n <GlobalStyles \/>\r\n <button>Toggle theme<\/button>\r\n <h1>It's a light theme!<\/h1>\r\n <footer>\r\n <\/footer>\r\n <\/>\r\n <\/ThemeProvider>\r\n );\r\n}\r\n\r\nexport default App;<\/code><\/pre>\n
ThemeProvider<\/code> component also gets imported and is passed the light theme (
lightTheme<\/code>) styles inside. We also import
GlobalStyles<\/code> to tighten everything up in one place.<\/p>\n
Now, the toggling functionality<\/h3>\n
useState<\/code> hook from
react<\/code>:<\/p>\n
\/\/ App.js\r\nimport React, { useState } from 'react';<\/code><\/pre>\n
\/\/ App.js\r\nconst [theme, setTheme] = useState('light');\r\n\r\n\/\/ The function that toggles between themes\r\nconst toggleTheme = () => {\r\n \/\/ if the theme is not light, then set it to dark\r\n if (theme === 'light') {\r\n setTheme('dark');\r\n \/\/ otherwise, it should be light\r\n } else {\r\n setTheme('light');\r\n }\r\n}<\/code><\/pre>\n
\/\/ App.js\r\nimport React, { useState } from 'react';\r\nimport { ThemeProvider } from 'styled-components';\r\nimport { lightTheme, darkTheme } from '.\/theme';\r\nimport { GlobalStyles } from '.\/global';\r\n\r\n\/\/ The function that toggles between themes\r\nfunction App() {\r\n const [theme, setTheme] = useState('light');\r\n const toggleTheme = () => {\r\n if (theme === 'light') {\r\n setTheme('dark');\r\n } else {\r\n setTheme('light');\r\n }\r\n }\r\n \r\n \/\/ Return the layout based on the current theme\r\n return (\r\n <ThemeProvider theme={theme === 'light' ? lightTheme : darkTheme}>\r\n <>\r\n <GlobalStyles \/>\r\n \/\/ Pass the toggle functionality to the button\r\n <button onClick={toggleTheme}>Toggle theme<\/button>\r\n <h1>It's a light theme!<\/h1>\r\n <footer>\r\n <\/footer>\r\n <\/>\r\n <\/ThemeProvider>\r\n );\r\n}\r\n\r\nexport default App;<\/code><\/pre>\n
How does it work?<\/h3>\n
\/\/ global.js\r\nbackground: ${({ theme }) => theme.body};\r\ncolor: ${({ theme }) => theme.text};\r\ntransition: all 0.25s linear;<\/code><\/pre>\n
GlobalStyles<\/code>, we assigned
background<\/code> and
color<\/code> properties to values from the
theme<\/code> object, so now, every time we switch the toggle, values change depending on the
darkTheme<\/code> and
lightTheme<\/code> objects that we are passing to
ThemeProvider<\/code>. The
transition<\/code> property allows us to make this change a little more smoothly than working with keyframe animations.<\/p>\n
Now we need the toggle component<\/h3>\n
Toggle<\/code> component and make our switch functionality reusable. That\u2019s one of the key benefits to making this in React, right?<\/p>\n
Toggle.js<\/code> and add the following: <\/p>\n
\/\/ Toggle.js\r\nimport React from 'react'\r\nimport { func, string } from 'prop-types';\r\nimport styled from 'styled-components';\r\n\/\/ Import a couple of SVG files we'll use in the design: https:\/\/www.flaticon.com\r\nimport { ReactComponent as MoonIcon } from 'icons\/moon.svg';\r\nimport { ReactComponent as SunIcon } from 'icons\/sun.svg';\r\n\r\nconst Toggle = ({ theme, toggleTheme }) => {\r\n const isLight = theme === 'light';\r\n return (\r\n <button onClick={toggleTheme} >\r\n <SunIcon \/>\r\n <MoonIcon \/>\r\n <\/button>\r\n );\r\n};\r\n\r\nToggle.propTypes = {\r\n theme: string.isRequired,\r\n toggleTheme: func.isRequired,\r\n}\r\n\r\nexport default Toggle;<\/code><\/pre>\n
theme<\/code> will provide the current theme (light or dark) and
toggleTheme<\/code> function will be used to switch between them. Below we created an
isLight<\/code> variable, which will return a boolean value depending on our current theme. We\u2019ll pass it later to our styled component.<\/p>\n
styled<\/code> function from styled-components, so let\u2019s use it. Feel free to add this on top your file after the imports or create a dedicated file for that (e.g. Toggle.styled.js) like I have below. Again, this is purely for presentation purposes, so you can style your component as you see fit.<\/p>\n
\/\/ Toggle.styled.js\r\nconst ToggleContainer = styled.button`\r\n background: ${({ theme }) => theme.gradient};\r\n border: 2px solid ${({ theme }) => theme.toggleBorder};\r\n border-radius: 30px;\r\n cursor: pointer;\r\n display: flex;\r\n font-size: 0.5rem;\r\n justify-content: space-between;\r\n margin: 0 auto;\r\n overflow: hidden;\r\n padding: 0.5rem;\r\n position: relative;\r\n width: 8rem;\r\n height: 4rem;\r\n\r\n svg {\r\n height: auto;\r\n width: 2.5rem;\r\n transition: all 0.3s linear;\r\n \r\n \/\/ sun icon\r\n &:first-child {\r\n transform: ${({ lightTheme }) => lightTheme ? 'translateY(0)' : 'translateY(100px)'};\r\n }\r\n \r\n \/\/ moon icon\r\n &:nth-child(2) {\r\n transform: ${({ lightTheme }) => lightTheme ? 'translateY(-100px)' : 'translateY(0)'};\r\n }\r\n }\r\n`;<\/code><\/pre>\n
lightTheme<\/code> is an active one, and if so, we move the appropriate icon out of the visible area \u2014 sort of like the moon going away when it\u2019s daytime and vice versa.<\/p>\n
ToggleContainer<\/code> component in Toggle.js, regardless of whether you\u2019re styling in separate file or directly in Toggle.js. Be sure to pass the
isLight<\/code> variable to it to specify the current theme. I called the prop
lightTheme<\/code> so it would clearly reflect its purpose.<\/p>\n
\/\/ App.js\r\n<Toggle theme={theme} toggleTheme={toggleTheme} \/>\r\n<h1>It's a {theme === 'light' ? 'light theme' : 'dark theme'}!<\/h1><\/code><\/pre>\n
\/\/ App.js\r\n<span>Credits:<\/span>\r\n<small><b>Sun<\/b> icon made by <a href=\"https:\/\/www.flaticon.com\/authors\/smalllikeart\">smalllikeart<\/a> from <a href=\"https:\/\/www.flaticon.com\">www.flaticon.com<\/a><\/small>\r\n<small><b>Moon<\/b> icon made by <a href=\"https:\/\/www.freepik.com\/home\">Freepik<\/a> from <a href=\"https:\/\/www.flaticon.com\">www.flaticon.com<\/a><\/small><\/code><\/pre>\n
The useDarkMode hook<\/h3>\n
src<\/code> directory and move our logic into this file with some tweaks:<\/p>\n
\/\/ useDarkMode.js\r\nimport { useEffect, useState } from 'react';\r\n\r\nexport const useDarkMode = () => {\r\n const [theme, setTheme] = useState('light');\r\n const toggleTheme = () => {\r\n if (theme === 'light') {\r\n window.localStorage.setItem('theme', 'dark')\r\n setTheme('dark')\r\n } else {\r\n window.localStorage.setItem('theme', 'light')\r\n setTheme('light')\r\n }\r\n };\r\n\r\n useEffect(() => {\r\n const localTheme = window.localStorage.getItem('theme');\r\n localTheme && setTheme(localTheme);\r\n }, []);\r\n\r\n return [theme, toggleTheme]\r\n};<\/code><\/pre>\n
localStorage<\/code>.<\/p>\n
useEffect<\/a><\/code> hook to check on component mounting. If the user has previously selected a theme, we will pass it to our
setTheme<\/code> function. In the end, we will return our
theme<\/code>, which contains the chosen
theme<\/code> and
toggleTheme<\/code> function to switch between modes.<\/p>\n
useDarkMode<\/code> hook. Go into App.js, import the newly created hook, destructure our
theme<\/code> and
toggleTheme<\/code> properties from the hook, and, put them where they belong:<\/p>\n
\/\/ App.js\r\nimport React from 'react';\r\nimport { ThemeProvider } from 'styled-components';\r\nimport { useDarkMode } from '.\/useDarkMode';\r\nimport { lightTheme, darkTheme } from '.\/theme';\r\nimport { GlobalStyles } from '.\/global';\r\nimport Toggle from '.\/components\/Toggle';\r\n\r\nfunction App() {\r\n const [theme, toggleTheme] = useDarkMode();\r\n const themeMode = theme === 'light' ? lightTheme : darkTheme;\r\n\r\n return (\r\n <ThemeProvider theme={themeMode}>\r\n <>\r\n <GlobalStyles \/>\r\n <Toggle theme={theme} toggleTheme={toggleTheme} \/>\r\n <h1>It's a {theme === 'light' ? 'light theme' : 'dark theme'}!<\/h1>\r\n <footer>\r\n Credits:<\/span>\r\n <small>Sun<\/b> icon made by smalllikeart<\/a> from www.flaticon.com<\/a><\/small>\r\n <small>Moon<\/b> icon made by Freepik<\/a> from www.flaticon.com<\/a><\/small>\r\n <\/footer>\r\n <\/>\r\n <\/ThemeProvider>\r\n );\r\n}\r\n\r\nexport default App;<\/code><\/pre>\n
useState<\/code> hook initiates the
light<\/code> theme initially. After that,
useEffect<\/code> runs, checks
localStorage<\/code> and only then sets the
theme<\/code> to
dark<\/code>.<\/p>\n
localStorage<\/code> in our
useState<\/code>: <\/p>\n
\/\/ useDarkMode.js\r\nconst [theme, setTheme] = useState(window.localStorage.getItem('theme') || 'light');<\/code><\/pre>\n
useState<\/code>, so let me show you a second solution, that I\u2019m using.<\/p>\n
componentMounted<\/code>. Then, inside the
useEffect<\/code> hook, where we check our
localTheme<\/code>, we\u2019ll add an
else<\/code> statement, and if there is no
theme<\/code> in
localStorage<\/code>, we\u2019ll add it. After that, we\u2019ll set
setComponentMounted<\/code> to
true<\/code>. In the end, we add
componentMounted<\/code> to our return statement. <\/p>\n
\/\/ useDarkMode.js\r\nimport { useEffect, useState } from 'react';\r\n\r\nexport const useDarkMode = () => {\r\n const [theme, setTheme] = useState('light');\r\n const [componentMounted, setComponentMounted] = useState(false);\r\n const toggleTheme = () => {\r\n if (theme === 'light') {\r\n window.localStorage.setItem('theme', 'dark');\r\n setTheme('dark');\r\n } else {\r\n window.localStorage.setItem('theme', 'light');\r\n setTheme('light');\r\n }\r\n };\r\n\r\n useEffect(() => {\r\n const localTheme = window.localStorage.getItem('theme');\r\n if (localTheme) {\r\n setTheme(localTheme);\r\n } else {\r\n setTheme('light')\r\n window.localStorage.setItem('theme', 'light')\r\n }\r\n setComponentMounted(true);\r\n }, []);\r\n \r\n return [theme, toggleTheme, componentMounted]\r\n};<\/code><\/pre>\n
theme<\/code> to the
localStorage<\/code>. I believe, that the best name for it will be
setTheme<\/code>, but we\u2019ve already used it, so let\u2019s call it
setMode<\/code>:<\/p>\n
\/\/ useDarkMode.js\r\nconst setMode = mode => {\r\n window.localStorage.setItem('theme', mode)\r\n setTheme(mode)\r\n};<\/code><\/pre>\n
\/\/ useDarkMode.js\r\nimport { useEffect, useState } from 'react';\r\nexport const useDarkMode = () => {\r\n const [theme, setTheme] = useState('light');\r\n const [componentMounted, setComponentMounted] = useState(false);\r\n\r\n const setMode = mode => {\r\n window.localStorage.setItem('theme', mode)\r\n setTheme(mode)\r\n };\r\n\r\n const toggleTheme = () => {\r\n if (theme === 'light') {\r\n setMode('dark');\r\n } else {\r\n setMode('light');\r\n }\r\n };\r\n\r\n useEffect(() => {\r\n const localTheme = window.localStorage.getItem('theme');\r\n if (localTheme) {\r\n setTheme(localTheme);\r\n } else {\r\n setMode('light');\r\n }\r\n setComponentMounted(true);\r\n }, []);\r\n\r\n return [theme, toggleTheme, componentMounted]\r\n};<\/code><\/pre>\n
Did the component mount?<\/h3>\n
componentMounted<\/code> property. We will use it to check if our component has mounted because this is what happens in
useEffect<\/code> hook. <\/p>\n
\/\/ App.js\r\nif (!componentMounted) {\r\n return <div \/>\r\n};<\/code><\/pre>\n
\/\/ App.js\r\nimport React from 'react';\r\nimport { ThemeProvider } from 'styled-components';\r\nimport { useDarkMode } from '.\/useDarkMode';\r\nimport { lightTheme, darkTheme } from '.\/theme';\r\nimport { GlobalStyles } from '.\/global';\r\nimport Toggle from '.\/components\/Toggle';\r\n\r\nfunction App() {\r\n const [theme, toggleTheme, componentMounted] = useDarkMode();\r\n\r\n const themeMode = theme === 'light' ? lightTheme : darkTheme;\r\n\r\n if (!componentMounted) {\r\n return <div \/>\r\n };\r\n\r\n return (\r\n <ThemeProvider theme={themeMode}>\r\n <>\r\n <GlobalStyles \/>\r\n <Toggle theme={theme} toggleTheme={toggleTheme} \/>\r\n <h1>It's a {theme === 'light' ? 'light theme' : 'dark theme'}!<\/h1>\r\n <footer>\r\n <span>Credits:<\/span>\r\n <small><b>Sun<\/b> icon made by <a href=\"https:\/\/www.flaticon.com\/authors\/smalllikeart\">smalllikeart<\/a> from <a href=\"https:\/\/www.flaticon.com\">www.flaticon.com<\/a><\/small>\r\n <small><b>Moon<\/b> icon made by <a href=\"https:\/\/www.freepik.com\/home\">Freepik<\/a> from <a href=\"https:\/\/www.flaticon.com\">www.flaticon.com<\/a><\/small>\r\n <\/footer>\r\n <\/>\r\n <\/ThemeProvider>\r\n );\r\n}\r\n\r\nexport default App;<\/code><\/pre>\n
Using the user\u2019s preferred color scheme<\/h3>\n