How To Implement Dark Mode in Your React App

Give your users’ eyes a rest

Matthew Croak
Dec 5, 2019 · 7 min read

While working on a React app, I wanted to implement a day/night mode feature that allows the user to select whether or not they want to use a light or dark theme. Dark mode is becoming increasingly popular in app and software design. Aside from looking pretty cool, dark mode can help with device-energy conservation and reducing eye strain. For more on the science behind dark mode, check out this article in The Observer.

In this post, I’ll demonstrate how easy it was for me to implement this new design trend in my React app. There’s a lot more CSS involved in my application, but in order to remain concise, I’ll only provide the CSS relevant to my day-/night-mode functionality. First, let me provide some context.

My React app is an online text editor. My App component has a white background and black for the font color. This is the consistent color scheme throughout my application. I really am pushing the boundaries of contemporary web design.

Below is my App component.

import React, {Component} from 'react';
import './App.css';
import SizeContainer from './components/SizeContainer.js';
import {withRouter} from 'react-router-dom';
import { connect } from 'react-redux';
const App = (props) => {
return (
<div className={!props.mode ? "App" : 'night'}>
<h1 style={{height: 50, marginBottom: 0, padding: 5}}>
React Editor
</h1>
<SizeContainer mode={props.mode}/>
</div>
);
}
const mapStateToProps = (state) => {
return {
mode: state.mode
};
}
export default withRouter(connect(mapStateToProps) (App));

As you can see, my App component has access to the mode prop via mapStateToProps. This prop is stored in my reducer and is accessible by any components I choose to export with connect. While Redux isn’t necessary for this functionality, I find it does make the process of applying the styles easier while keeping my components lean and mean. For more on Redux, check out the documentation.

Below is my reducer:

export default function rootReducer(state={mode: false}, action){
switch(action.type){
case "CHANGE_MODE":
var mode = !state.mode
return {...state, mode}
default:
return state
}
}

When CHANGE_MODE is called, all that needs to be done is to get the current value of mode in my reducer’s state (true/false) and assign it the opposite value of what it currently is.

If state.mode is true, then !state.mode will equal false — and vice versa. No payload is necessary for this action. Now that we see how this is handled in the reducer, let’s take a look at the CHANGE_MODE action in our actions file.

export function changeMode(){  
return {
type: "CHANGE_MODE"
}
}

Since there’s no payload and the toggle operation is happening in the reducer, all we need to do is call this function in one of our components whenever we want to toggle the theme of the app. This brings me to my next component: SizeContainer.

Since I’m passing props.mode from the App to the SizeContainer, the child elements in the SizeContainer will also have access to the props.mode. This is important for one child component in particular: Buttons.

import React from 'react';
import {FaFileCode, FaLightbulb} from "react-icons/fa";
import {withRouter} from 'react-router-dom';
import { connect } from 'react-redux';
import { changeMode } from './../actions'
const Buttons = (props) => { var {mode, changeMode} = props; return(
<div className='buttonsContainer' >
<div>
<div className='buttons' onClick={changeMode}>
<FaLightbulb/>
<span className="tooltiptext">
{mode ? "Day Mode" : "Night Mode"}
</span>
</div>
<div className='buttons'>
<FaFileCode/>
<span className="tooltiptext">
Download Code
</span>
</div>
</div>
</div>
)
};
export default withRouter(connect(null, {changeMode}) (Buttons))

The above code will render the below two button — one has a light-bulb icon, the other a code-file icon.

As you can probably guess, the button with the light bulb icon will control the dark-mode toggle. No need to worry about the second button for now. Pay attention to the below line.

import { changeMode } from './../actions'

This is the changeMode function that gets exported from our actions. This function is added the light bulb button as an onClick event. Click the button, and execute the action to change the mode. Simple, no?

The tool tip for the light-bulb button will say either “Day Mode” if props.mode is true (meaning, dark mode is active), or if it’s false (dark mode is off), it’ll read “Night Mode”.

In this instance, dark mode has not been initiated — hence the white background and “Night Mode” tooltip

Now that we have our React app set up, let’s get to the CSS.

.App {  
text-align: center;
height: 100%;
transition: background-color 0.3s, color 0.3s;
}

When dark mode isn’t selected, our CSS is pretty straightforward. Since white is the default background color and black is the default font color of HTML, we don’t need to specify them here.

All we need to specify is the height and width of the App (we want it to take up the whole page) and the transition.

By specifying a background-color and color transition (both 0.3s), this will ensure our transition from day to night mode is smooth. Otherwise, it’ll look more like a jarring flash than a smooth transition from day to night.

Now that we have our default styles, let’s get down to business with dark mode CSS.

.night {
text-align: center;
height: 100%;
width: 100%;
background-color: black;
transition: background-color 0.3s, color 0.3s;
}

We declare the styles for the .night class name and explicitly state the background color and color as to prevent the HTML from using the default values. This one also has a transition: background-color 0.3s, color 0.3s; This will ensure that when we transition from dark back to light mode, the transition will remain smooth.

If you remember from earlier in the article, our App component’s className is determined by the value of props.mode. If props.mode is false, then use className="App"; otherwise, use className="night".

The above CSS satisfies the theme change for our App component, but to ensure that it applies the necessary styles to all child elements, we need to add a few more lines.

.night h1, .night h2, .night div, .night textarea{
background-color: black;
color: white;
transition: background-color 0.3s, color 0.3s;
}

This will apply the background color and text color changes to any h1, h2, p, div, and textarea elements that fall within our App component.

These elements comprise the app’s text editor sections (textarea) and their titles (h2). The h1 is the title of the app itself, and pretty much everything is housed either in its own or in a parent’s div container.

By putting .night in front of all of these elements, we can target these specific child tags when we change the theme.

It’s also worth noting that here we use a descendant selector, identified by the space between .night and an HTML tag. This will select all child instances of an element that falls under .night. We don’t use >, the child selector, as it would only select immediate children of an element. We also don’t use + , the adjacent sibling selector, which would only select siblings. For more on CSS combinators, check out this really awesome article.

We’re almost done, but there are a few more things we need to consider. First, there are a few elements don’t want to change the color or background-color of (at least for now), such as the navy blue buttons. Whether or not the mode is dark or light, I want them to remain navy blue and have white icons. To ensure the className switch doesn’t affect these buttons, I can add the following CSS.

.night .buttons{  background-color: navy;}

By putting this line in my App.css file, I can prevent the previously mentioned CSS from changing the buttons’ styles. Previously, I thought that this line had to come after the original .night CSS since CSS is cascading by design and uses precedence to determine the style applied to an element.

In reality, where I put this line does not matter. Since we are using a class selector to target the buttons, the .night div styles will not be applied as the level of specificity is lower than .night .buttons. For more information, please read the MDN docs on specificity.

The above logic can also be applied for my last element, which I would like to keep independent from dark mode: my output container. This section, which has the class name of .view, is where the user’s code (composed of by combining the top three sections) will be running, and since that output area should only reflect the code written by the user, we should ensure that the default styles are enforced unless the user types new ones in the CSS section.

.night .view {  background-color: white;}

The rest of the output will be inserted into an iframe, which is a child of .view. This means the styles from the user will be completely separate from that of my app’s. Preserving the background color of the output container is all that’s needed.

Below is our final result.

There you have it. With just a few extra lines of CSS and a little bit of props logic, your React app can have dark mode implemented efficiently and effectively.

Better Programming

Advice for programmers.

Matthew Croak

Written by

UI Engineer at Analytic Partners 📲 💻 📈 | Dog Lover 🐶

Better Programming

Advice for programmers.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade