Dark Theme with Styled Components
The what & the why
The saying goes: “written in black and white”. But can it be the other way around?
Let’s talk about dark theme (AKA dark mode). Dark-theming your app means adding an option to invert your app’s colors. Whereas traditional apps use a light background and a dark text color, some users would prefer to see it the other way around. It can be just a matter of taste, but it can also increase your app’s accessibility, be easier on the eyes, and reduce the battery usage of your customers’ devices. It’s quite simple to add it to any existing app.
At BigPanda, we use React and styled-components. These two are quite popular, and their combination can easily be used to bootstrap a dark theme-able app. Let’s do it step-by-step.
You can see the end result in CodeSandbox.
Project bootstrapping
We’ll start by creating a new React project, using create-react-app
:
npx create-react-app darktheme
cd darktheme
npm i
npm start
Now we have a boilerplate app. Let’s edit App.jsx
, so all we have is just a page title and a toggling button:
App.jsx
import React, { useState } from 'react';const App = () => {
const [theme, setTheme] = useState("light");
const isDarkTheme = theme === "dark";
const toggleTheme = () => setTheme(isDarkTheme ? "light" : "dark");
return (
<div>
<h1>Hello!</h1>
<button onClick={toggleTheme}>
{isDarkTheme ?
<span aria-label="Light mode" role="img">🌞</span> :
<span aria-label="Dark mode" role="img">🌜</span>}
</button>
</div>
);
}export default App;
Cool. We can see that the toggling mechanism is working, since the button’s emoji is changing. Now let’s add styled-components to the mix.
Styled Components in a nutshell
styled-components is a popular CSS-in-JS solution. It’s a great library, that can solve many of our common CSS problems, like class names bleeding or vendor prefixing. In our case, it’s great for theming management. We’ll start by installing it:
npm i styled-components
We’ll use two tools that are baked into styled-components: ThemeProvider & createGlobalStyle.
ThemeProvider
will wrap our entire application, and provide the theme’s context to all of the nested components.createGlobalStyle
will enable us to set the global styles, e.g. the body and text color.
Let’s create a new file in the src
directory, named theme.js
. It will include our global styles and our two color palettes:
theme.js
import { createGlobalStyle } from 'styled-components';export const GlobalStyles = createGlobalStyle`
body {
background: ${({ theme }) => theme.body};
color: ${({ theme }) => theme.text};
transition: background 0.2s ease-in, color 0.2s ease-in;
}
`;export const lightTheme = {
body: '#f1f1f1',
text: '#121620'
};export const darkTheme = {
body: '#121620',
text: '#f1f1f1'
};
As you can see, styled components’ syntax is very similar to regular CSS. It uses template literals, which is a cool concept on its own, to set the styling rules of HTML directives & React components. The transition
rule will help us animate between the dark and the light themes, and the background and text colors will be set by the selected theme. The light theme has a light background color and a dark text color, and the dark theme has the exact opposite settings.
In App.jsx
, we’ll import GlobalStyles
, lightTheme
and darkTheme
, and use ThemeProvider
to pass the selected theme to the child components:
App.jsx
import React, { useState } from 'react';
import { ThemeProvider } from 'styled-components';import { lightTheme, darkTheme, GlobalStyles } from './theme';const App = () => {
const [theme, setTheme] = useState("light");
const isDarkTheme = theme === "dark";
const toggleTheme = () => setTheme(isDarkTheme ? "light" : "dark");
return (
<ThemeProvider theme={isDarkTheme ? darkTheme : lightTheme}>
<>
<GlobalStyles />
<h1>Hello!</h1>
<button onClick={toggleTheme}>
{isDarkTheme ?
<span aria-label="Light mode" role="img">🌞</span> :
<span aria-label="Dark mode" role="img">🌜</span>}
</button>
</>
</ThemeProvider>
);
}export default App;
Notice that we’re passing the selected theme as the theme
property, so the destructuring in GlobalStyles
will work. This theme
property will be available in all of the child components, so you don’t have to worry about prop-drilling. You can apply the same logic that we used in GlobalStyles
in all of your components, to color anything that’s not inherited from the body’s settings (for example, an input component’s text color).
...and that’s it! We have a dark-themed application!
Bonus points: using the user’s preference
Now our app uses a light theme as its default, and we offer our users an option to switch to the dark theme. That’s great progress. But we can improve it even further, by detecting that a user prefers dark mode, based on his/her system setting, and auto-setting it by default. All thanks to prefers-color-scheme.
In App.jsx
, we’ll add a React hook, that will check if the user prefers dark mode. If so, we’ll override the default light mode value. In addition, we can tweak toggleTheme
a bit, and save the selected theme in local storage, to make sure that any user interaction will override the default preference:
const toggleTheme = () => {
const updatedTheme = isDarkTheme ? "light" : "dark";
setTheme(updatedTheme);
localStorage.setItem("theme", updatedTheme);
};useEffect(() => {
const savedTheme = localStorage.getItem("theme");
const prefersDark = window.matchMedia &&
window.matchMedia('(prefers-color-scheme: dark)').matches;
if (savedTheme && ["dark", "light"].includes(savedTheme)) {
setTheme(savedTheme);
} else if (prefersDark) {
setTheme("dark");
}
}, []);
Now your app is more accessible, has a better user experience, and it looks great in dark mode. Not too shabby for just one library & a few lines of code!