Dark mode in React with Theme UI

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

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.js
    module.exports = {
    plugins: ["@affectionatedoor/gatsby-theme-ui"]
    };
  • Use the Layout component from my theme in the index page:

    // src/pages/index.js
    import 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:

homepage with my gatsby-theme-ui

You can also try to build and serve the app locally with yarn build && 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

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:

// gatsby-config.js
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:

// src/gatsby-plugin-theme-ui
export default {
  useCustomProperties: true,  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:

// src/pages/index.js
/** @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 its name. 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:

// src/components/theme-switcher.js
/** @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:

homepage with the official theme

Right now I can see the flash, that I mentioned earlier, and I think it’s a problem with the official theme. If it’s something on my part, I will edit the instructions.

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). Brent Jackson, if you’re reading this post, please add the quote to the Theme UI wall. Thanks.

Other things to read

Popular notes

Other posts