Dark mode in React with Theme UI
Table of contents
A few months ago, I searched for the best dark mode solution for a React application. I wrote a post about that if you are interested. But that was three months ago, and three months is a long time in web development—not really, just trying to create some drama. Nowadays, the solution to dark mode is a lot easier and has a name: Theme UI.
With the Theme UI library, you can style your applications consistently with a design system (styled-system) and a CSS-in-JS library (emotion). Additionally, Theme UI offers support for MDX, and who doesn’t love MDX? In the following examples, I will use Gatsby because the MDX support is great. But you can use MDX with many other React apps; you don’t have to use Gatsby.
Let’s see now in practice how you can add dark mode to your site with Theme UI.
The easy way
GitHub repository. Checkout to the with-custom-theme
branch.
The easy way to add Theme UI in your application is to use a Gatsby theme I made. There’s nothing special about that theme; it just setups Theme UI and exposes some UI components I use in my projects. Those components include background images, animation on scroll components, buttons, and many others.
But before you can enjoy dark mode in your application, you’ll have to do 4 things:
Create a new
hello-world
starter:gatsby new . git@github.com:gatsbyjs/gatsby-starter-hello-world.git
Install my theme:
yarn add @affectionatedoor/gatsby-theme-ui
Add it as a plugin in
gatsby-config.js
:gatsby-config.jsmodule.exports = { plugins: ["@affectionatedoor/gatsby-theme-ui"] };
Use the
Layout
component from my theme in the index page:src/pages/index.jsimport React from "react"; import Layout from "@affectionatedoor/gatsby-theme-ui/src/components/Layout"; export default () => <Layout>Hello world!</Layout>;
If you start the development server and visit the homepage, you will see this screen:
You can also try to build and serve the app locally with yarn build
and yarn serve
. You will not experience any of the problems with the context that I mentioned in my previous post. Theme UI implements the last solution I showed with the CSS custom properties, the same solution Dan Abramov uses for his personal blog. Not by default, but with a small option—you’ll see that in a bit.
But you may not need all the components I have stuffed inside that theme. You may just don’t want to use my stinky theme. That’s fine, I don’t blame you. Instead, you may want something less opinionated. If that’s the case, you can use the official Gatsby Theme UI theme.
Using the official theme
GitHub repository - master branch.
The official Theme UI theme for Gatsby, let’s refer to it as the official theme, doesn’t do a lot of things; it’s the definition of minimal. It wraps Gatsby’s root element with a ThemeProvider, and it creates some empty files for the theme and the custom MDX components (don’t worry about the last, they are not important). We can then shadow those empty files in our app, and fill them with our code.
But before we do that, let’s install the dependencies first:
yarn add theme-ui gatsby-plugin-theme-ui @emotion/core @mdx-js/react
Then, add the plugin in the plugin array:
module.exports = {
plugins: ["gatsby-plugin-theme-ui"],
};
The next thing you’ll have to do is overwrite, or shadow, the empty theme file from the official theme:
export default {
useCustomProperties: true,
initialColorMode: "light",
colors: {
text: "#000",
background: "#fff",
primary: "#07c",
modes: {
dark: {
text: "#fff",
background: "#000",
primary: "#0cf",
},
},
},
};
You can add a ton of useful stuff inside the theme object, but here I add only the colors, in an attempt to be minimal like the official theme. Also, notice the useCustomProperties
field I highlighted in the code above. I said before that it’s a good idea to use CSS custom properties. If you don’t use them, and you change the theme from light to dark in production, when you reload the page, you’ll see a brief flash. That flash is caused because you see momentarily the light theme before the dark—your chosen theme in this case. It has to do with how React’s hydrate method works.
Now that you have a basic theme, it’s time to use it in your code. Open the index page, and paste the following code:
/** @jsx jsx */
import { jsx, Styled } from "theme-ui";
import ThemeSwitcher from "../components/theme-switcher";
export default () => {
return (
<div
sx={{
backgroundColor: "background",
color: "text",
minHeight: "100vh",
display: "flex",
alignItems: "center",
justifyContent: "center",
flexDirection: "column",
}}
>
<ThemeSwitcher />
<Styled.h1>Hello world!</Styled.h1>
</div>
);
};
If you are not familiar with emotion, you may be wondering what the @jsx jsx
comment does at the top of the file. With that comment, you declare that you want to use the custom JSX pragma from Theme UI. What this thing does is not as scary as it sounds. Let me explain.
Your JSX code at build time is transformed to React.createElement function calls. For example, this div
:
<div style={{ color: "red" }}>Hello World</div>
During build time becomes this:
React.createElement(
"div",
{
style: {
color: "red",
},
},
"Hello World"
);
If you don’t believe me, you can watch this process real-time with the Babel Online compiler. The createElement
function takes 3 arguments: the component type, the props, and the children.
So, coming back to the custom pragma, instead of using the React.createElement
function, we use the jsx
function from Theme UI. For example, the React.createElement
you saw in the previous code snippet becomes:
import { jsx } from "theme-ui";
jsx(
"div",
{
style: {
color: "red",
},
},
"Hello World"
);
We do that because we want to use the sx prop from Theme UI—which is not a standard JSX property like the style
for example—to style our components. Speaking of the sx
prop, I use it on the index page to position the content of the div at the center of the screen. I also use a ThemeSwitcher
component that doesn’t exist yet. This component is a button that switches our themes, as the name suggests.
Let’s now create that ThemeSwitcher
component. Create a new file at src/components/theme-switcher.js
and paste the following code:
/** @jsx jsx */
import { jsx, useColorMode } from "theme-ui";
const ThemeSwitcher = () => {
const [colorMode, setColorMode] = useColorMode();
const nextColorMode = colorMode === "light" ? "dark" : "light";
return (
<button
sx={{
position: "absolute",
top: 3,
right: 3,
backgroundColor: "primary",
color: "background",
border: 0,
px: 3,
py: 2,
cursor: "pointer",
}}
onClick={() => setColorMode(nextColorMode)}
>
Toggle mode
</button>
);
};
export default ThemeSwitcher;
In the ThemeSwitcher
component, I use the sx
prop to style the button because no one likes ugly buttons, and I also use the useColorMode
hook that allows us to change the theme.
And that’s it. If you now start the development server, this is what you’ll see:
Wrapping up, I want to say that few times I feel joy using a JavaScript library. When I use Theme UI, it’s one of those times (lol).
Other things to read
Popular
- Reveal animations on scroll with react-spring
- Gatsby background image example
- Extremely fast loading with Gatsby and self-hosted fonts