More and more, we are seeing the dark mode feature in the apps that we are using every day. From mobile to web apps, the dark mode has become necessary for companies that want to take care of their user's eyes. Indeed, having a bright screen at night is really painful for our eyes. By turning (automatically) the dark mode helps reduce this pain and keep our users engage with our apps all night long (or not).
In this post, we are going to see how we can easily implement a dark mode feature in a ReactJS app. In order to do so, we'll leverage some React features like context, function components, and hooks.
Too busy to read the whole post? Have a look at the CodeSandbox demo to see this feature in action along with the source code.
What Will You Learn?
At this end of this post, you will be able to:
- Combine React
Context
and theuseReducer
hook to share a global state throughout the app. - Use the
ThemeProvider
from thestyled-components
library to provide a theme to all React components within our app. - Build a dark mode feature into your React app in an easy and non-intrusive way.
What Will You Build?
In order to add the dark mode feature into our app, we will build the following features:
- A
Switch
component to be able to enable or disable the dark mode. - A dark and light theme for our styled-components to consume.
- A global
Context
andreducer
to manage the application state.
Theme Definition
The first thing that we need for our dark mode feature is to define the light and dark themes of our app. In other words, we need to define the colors (text, background, ...) for each theme.
Thanks to the styled-components
library we are going to use, we can easily define our themes in a distinct file as JSON objects and provide it to the ThemeProvider
later.
Below is the definition of the light and dark themes for our app:
const black = "#363537";
const lightGrey = "#E2E2E2";
const white = "#FAFAFA";
export const light = {
text: black,
background: lightGrey
};
export const dark = {
text: white,
background: black
};
As you can notice, this is a really simplistic theme definition. It's up to you to define more theme parameters to style the app according to your visual identity.
Now that we have both our dark and light themes, we can focus on how we’re going to provide them to our app.
Theme Provider
By leveraging the React Context API, the styled-components
provides us a ThemeProvider
wrapper component. Thanks to this component, we can add full theming support to our app. It provides a theme to all React components underneath itself.
Let's add this wrapper component at the top of our React components' tree:
import React from "react";
import { ThemeProvider } from "styled-components";
export default function App() {
return (
<ThemeProvider theme={...}>
...
</ThemeProvider>
);
};
You may have noticed that the ThemeProvider
component accepts a theme property. This is an object representing the theme we want to use throughout our app. It will be either the light or dark theme depending on the application state. For now, let's leave it as is as we still need to implement the logic for handling the global app state.
But before implementing this logic, we can add global styles to our app.
Global Styles
Once again, we are going to use the styled-components
library to do so. Indeed, it has a helper function named createGlobalStyle
that generates a styled React component that handles global styles.
import React from "react";
import { ThemeProvider, createGlobalStyle } from "styled-components";
export const GlobalStyles = createGlobalStyle`...`;
By placing it at the top of our React tree, the styles will be injected into our app when rendered. In addition to that, we'll place it underneath our ThemeProvider
wrapper. Hence, we will be able to apply specific theme styles to it. Let's see how to do it.
export const GlobalStyles = createGlobalStyle`
body, #root {
background: ${({ theme }) => theme.background};
color: ${({ theme }) => theme.text};
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
font-family: BlinkMacSystemFont, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
}
`;
export default function App() {
return (
<ThemeProvider theme={...}>
<>
<GlobalStyles />
...
</>
</ThemeProvider>
);
};
As you can see, the global text and background color are provided by the loaded theme of our app.
It's now time to see how to implement the global state.
Global State
In order to share a global state that will be consumed by our components down the React tree, we will use the useReducer
hook and the React Context
API.
As stated by the ReactJS documentation, Context
is the perfect fit to share the application state of our app between components.
Context provides a way to pass data through the component tree without having to pass props down manually at every level.
And the useReducer
hook is a great choice to handle our application state that will hold the current theme (light or dark) to use throughout our app.
This hook accepts a reducer
and returns the current state paired with a dispatch
method. The reducer is a function of type (state, action) => newState
that manage our state. It is responsible to update the state depending on the type of action that has been triggered. In our example, we will define only one type of action called TOGGLE_DARK_MODE
that will enable or disable the dark mode.
Let's create this reducer function in a separate file, reducer.js
:
const reducer = (state = {}, action) => {
switch (action.type) {
case "TOGGLE_DARK_MODE":
return {
isDark: !state.isDark
};
default:
return state;
}
};
export default reducer;
As you may have noticed, our state is holding a single boolean variable isDark
. If the TOGGLE_DARK_MODE
action is triggered, the reducer updates the isDark
state variable by toggling is value.
Now that we have our reducer
implemented we can create our useReducer
state and initialize it. By default, we will disable the dark mode.
import React, { useReducer } from "react";
import reducer from "./reducer";
export default function App() {
const [state, dispatch] = useReducer(reducer, {
isDark: false
});
...
};
The only missing piece in our global state implementation is the Context. We'll also define it in a distinct file and export it, context.js
:
import React from "react";
export default React.createContext(null);
Let's now combine everything together into our app and use our global state to provide the current theme to the ThemeProvider
component.
import React, { useReducer } from "react";
import { ThemeProvider, createGlobalStyle } from "styled-components";
import { light, dark } from "./themes";
import Context from "./context";
import reducer from "./reducer";
...
export default function App() {
const [state, dispatch] = useReducer(reducer, {
isDark: false
});
return (
<Context.Provider value={{ state, dispatch }}>
<ThemeProvider theme={state.isDark ? dark : light}>
<>
<GlobalStyles />
...
</>
</ThemeProvider>
</Context.Provider>
);
};
As you can see the Context
is providing, through its Provider
, the current application state and the dispatch method that will be used by other components to trigger the TOGGLE_DARK_MODE
action.
The Switch Component
Well done 👏👏 on completing all the steps so far. We are almost done. We’ve implemented all the logic and components needed for enabling the dark mode feature. Now it’s time to trigger it in our app.
To do so, we'll build a Switch
component to allow users to enable/disable dark mode. Here's the component itself:
import React from "react";
import Context from "./context";
import styled from "styled-components";
const Container = styled.label`
position: relative;
display: inline-block;
width: 60px;
height: 34px;
margin-right: 15px;
`;
const Slider = styled.span`
position: absolute;
top: 0;
display: block;
cursor: pointer;
width: 100%;
height: 100%;
background-color: #ccc;
border-radius: 34px;
-webkit-transition: 0.4s;
transition: 0.4s;
&::before {
position: absolute;
content: "";
height: 26px;
width: 26px;
margin: 4px;
background-color: white;
border-radius: 50%;
-webkit-transition: 0.4s;
transition: 0.4s;
}
`;
const Input = styled.input`
opacity: 0;
width: 0;
height: 0;
margin: 0;
&:checked + ${Slider} {
background-color: #2196f3;
}
&:checked + ${Slider}::before {
-webkit-transform: translateX(26px);
-ms-transform: translateX(26px);
transform: translateX(26px);
}
&:focus + ${Slider} {
box-shadow: 0 0 1px #2196f3;
}
`;
const Switch = () => {
const { dispatch } = useContext(Context);
const handleOnClick = () => {
// Dispatch action
dispatch({ type: "TOGGLE_DARK_MODE" });
};
return (
<Container>
<Input type="checkbox" onClick={handleOnClick} />
<Slider />
</Container>
);
};
export default Switch;
Inside the Switch
component, we are using the dispatch
method from the Context
to toggle the dark mode theme.
Finally, let's add it to the app.
export default function App() {
...
return (
<Context.Provider value={{ state, dispatch }}>
<ThemeProvider theme={state.isDark ? dark : light}>
<>
<GlobalStyles />
<Switch />
</>
</ThemeProvider>
</Context.Provider>
);
};
Conclusion
Dark mode has been a highly requested feature, and we successfully added support for it in our React application by using some of the latest React features. I hope this post will help you add dark mode capability to your app and save the eyes of your users.