Style a React app using Theme UI (theme-ui) package

Theme UI (theme-ui) is a library for styling React components using a customisable theme object. It is easier to get started with theme-ui rather than explaining it. In this article, we will cover the following topics:

  • Getting started with theme-ui
  • Defining fonts and colors
  • Variants for heading, button, etc
  • Responsive breakpoints and Media queries
  • Dark mode

We will build a sample app using theme-ui. The code for the sample app is available in a github repo. And it is hosted in Netlify. The sample app that we build is a website that organises “nature tours”. There is a nav bar that has the name of the company. The nav bar also has an icon that serves as a button. It switches between the default bright mode and a dark mode. Beneath the nav bar is an image. Centered within the image is a heading (h1) tag and a button that calls for action. Below the image is a section that lists upcoming tours. This section is responsive. In desktop mode, there are three tours per row. When the user views the app in a tablet, there are two rows per row. And for a mobile, we vertically stack the tours one below another.

theme-ui demo app
theme-ui demo app

A) Getting started with theme-ui

We will use the default CRA project template:

npx create-react-app theme-ui-demo

Add theme-ui to the project.

yarn add theme-ui

In src folder, create a theme.js file that will export the theme object.

const theme = {};
export default theme;

In src/index.js file, import the ThemeProvider component. Wrap the App component with the ThemeProvider. Supply the newly created theme object to the ThemeProvider.

import { ThemeProvider } from 'theme-ui';
import theme from './theme';
...

    <ThemeProvider theme={theme}>
      <App />
    </ThemeProvider>

In src/App.js, add the following code to the top of the file:

/** @jsxImportSource theme-ui */

While using theme-ui, every file that uses the sx prop (see below) should have this comment as the first line of the file. Modify the App.js component to use the sx prop.

return (
  <div sx={{ 
    display: 'flex',
    justifyContent: 'center',
    alignItems: 'center',
    height: '100vh'
  }}>
    Hello world
  </div>
);

All of the above code is available in a CodeSandbox.

B) Defining fonts and colors

In the theme object, add a fonts key with the following fonts.

const theme = {
  fonts: {
    body: 'Montserrat',
    heading: 'Raleway',
  },
}

For body tags like paragraph, list, etc., we use the Montserrat font. And for heading tags like h1, h2, h3, etc., we use the Raleway font. These fonts may not be available in the user’s system. So, our web app should download these fonts from a source like Google font library. In index.html, add the following lines of code to let the user browser to download these fonts.

<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
  href="https://fonts.googleapis.com/css2?family=Montserrat&family=Raleway:wght@700&display=swap"
  rel="stylesheet"
/>

The above code is available when you pick fonts from Google font library.

Apart from font family, we can also define font sizes for our app.

fontSizes: [12, 14, 16, 20, 24, 32, 48],

Let’s define some colors in the theme object.

colors: {
  text: '#000',
  background: '#fff',
  primary: '#0552ba',
  navbar: 'rgba(5, 82, 186, .7)',
  white: '#fff'
},

The navbar color is the same as the primary color but with a 70% opacity. There is one more key that we should add to the theme object. It is the space key. This key helps us define values for margins and paddings.

space: [0, 4, 8, 16, 32, 64, 128, 256, 512],

How to reference theme object values from the component

Now, it is time to define the navbar in the App component. In App.js file, add the following code.

<header
  sx={{
    position: 'fixed',
    width: '100%',
    backgroundColor: 'navbar',
    height: 64,
    display: 'flex',
    justifyContent: 'space-between',
    alignItems: 'center',
    zIndex: 1,
  }}
>
  <div
    sx={{
      fontFamily: 'heading',
      fontSize: 3,
      color: 'white',
      pl: 4,
    }}
  >
    Exotic Expeditions
  </div>
</header>

The navbar is fixed to the top of the page. And it has a height of 64 pixels. Setting a zIndex of 1 ensures that it is at the top of the page over all the page content. The most interesting property in the header element is the backgroundColor. Instead of defining a color, it refers to a key in the colors object within the theme object. In this example, the navbar key refers to a color of rgba(5, 82, 186, .7).

The other interesting properties are found in the div element that has the text: ‘Exotic Expeditions’. It has a font family of heading. Again, this refers to a key in the fonts object within the theme. There is also the color property. Though it has the common color name of white, it actually refers to a key in the colors object within the theme.

The fontSize property is a number 3 that looks a little low. It actually refers to the index of the font size in the fontSizes collection within the theme. So, 3 refers to a font size of 20 pixels.

fontSizes: [12, 14, 16, 20, 24, 32, 48],

There is a pl property, shorthand for padding left. For other directions, there are similar shorthands: pr, pt, pb. If we want to define the same values for padding left and padding right, we can use the px property. Similarly there is a py key. In our example, there is a pl with a value of 4. This refers to the index of the element in the space collection within the theme. A pl of 4 implies a paddingLeft of 32 pixels.

space: [0, 4, 8, 16, 32, 64, 128, 256, 512],

It is time to tackle the next key topic: variants.

C) Variants for headings and button

We will now add a main tag. The main tag has a div element that covers the entire width and 70% of the window height. Centered within the div element is a h1 tag and a button that invites users to join a tour. We won’t handle the button click event as our main focus is on styles. The code in the App component looks like so:

<main>
  <div
    sx={{
      width: '100vw',
      height: '70vh',
      position: 'relative',
      display: 'flex',
      justifyContent: 'center',
      alignItems: 'center',
    }}
  >
    <div
      sx={{
        position: 'absolute',
        zIndex: -1,
        backgroundImage: 'url("/travel.jpg")',
        filter: 'blur(2px)',
        width: '100%',
        height: '100%',
      }}
    ></div>
    <div
      sx={{
        position: 'absolute',
        zIndex: -1,
        backgroundColor: 'imageOverlay',
        width: '100%',
        height: '100%',
      }}
    ></div>
    <div sx={{ textAlign: 'center' }}>
      <h1 sx={{ variant: 'styles.h1', color: 'hero' }}>Be one with nature</h1>
      <button sx={{ variant: 'styles.button' }}>Join us</button>
    </div>
  </div>
</main>

In public folder, there should be an image with name: travel.jpg. Grab an image from unsplash.com and place it in the public folder. There are two new colors as well: imageOverlay and hero. Add these colors to the theme object (theme.js).

imageOverlay: 'rgba(5, 82, 186, .2)',
hero: '#ffdab9',

The main topic of discussion in this section is variants. There are two variants in the sx props: styles.h1 and styles.button. We define the styles for a variant in the theme object. For all headings (h1, h2, h3, etc), we have a variant: styles.heading.

styles: {
  heading: {
    fontFamily: 'heading',
    margin: 0,
    mb: 3,
    lineHeight: 'heading',
    color: 'text',
  },
  h1: {
    fontSize: 6,
    variant: 'styles.heading',
  },
  h2: {
    fontSize: 5,
    variant: 'styles.heading',
  },
  h3: {
    fontSize: 4,
    variant: 'styles.heading',
  },
}

Note that the key with name ‘styles’ is upto us. We can have this key with name ‘someKeyName’ and reference the variant as someKeyName.heading.

Our heading variant has a font family that refers to Raleway. It has a margin bottom of 16px and a color of black.

The h1 variant has a font size of 48 pixels. Whereas h2 and h3 variants have a font size of 32 pixels and 24 pixels respectively. The name of the variant can be anything. For example, the h1 tag can have a variant name of someKeyName.heading1. It is upto us to define meaningful keys for variants.

Once defined, we can use these variants in multiple places in our component.

Button variant

Now, let’s see the code for a more complex variant: button.

button: {
  backgroundColor: 'primary',
  color: 'white70',
  px: 4,
  py: 3,
  border: 'none',
  outline: 'none',
  cursor: 'pointer',
  fontSize: 2,
  borderRadius: 4,
  transition: 'all 200ms',
  ':focus': {
    outline: '1px solid',
  },
  ':hover': {
    color: 'white',
    backgroundColor: 'highlight',
  },
},

The background color of button is primary (‘#0552ba’) but it changes to highlight (‘#002366’) on hover. Initial color of the button is white70 (‘rgba(255, 255, 255, 0.7)’) and that changes to white on hover. Note how we define the hover styles with ‘:hover’ key. A similar key for focus (‘:focus’) exists that sets an outline of 1 pixel when the button receives focus. The other styles are self-explanatory. For example, it is easy to infer that the button has a horizontal padding of 32 pixels and a vertical padding of 16 pixels.

D) Responsive breakpoints and Media queries

Below the background image, we have a section for upcoming tours. This section maps an array of tours to cards. In App.js, just before the closing main tag, add this code.

<div
  sx={{
    pl: [5, 5, 5, 6, 7],
    pr: [4, 4, 4, '96px', '224px'],
    py: 5,
    backgroundColor: 'secondary',
    '@media screen and (max-width: 480px)': {
      textAlign: 'center',
    },
  }}
>
  <h2
    sx={{
      variant: 'styles.h2',
      pl: 3,
      pr: '48px',
    }}
  >
    Upcoming tours
  </h2>
  <div sx={{ display: 'flex', flexWrap: 'wrap' }}>
    {tours.map((t) => (
      <div
        key={t.title}
        sx={{
          variant: 'styles.card',
          position: 'relative',
          width: [
            'calc(100% - 32px)',
            'calc(50% - 32px)',
            'calc(33.33% - 32px)',
            'calc(33.33% - 32px)',
            'calc(33.33% - 32px)',
          ],
        }}
      >
        <h3 sx={{ variant: 'styles.h3' }}>{t.title}</h3>
        <p sx={{ variant: 'styles.p' }}>{t.content}</p>
        <button
          sx={{
            variant: 'styles.button',
            py: 2,
            position: 'absolute',
            bottom: 16,
            right: 16,
          }}
        >
          Join us
        </button>
      </div>
    ))}
  </div>
</div>

When you read this code, you probably recognise most of it by now. So, I will go over the ones that are difficult to understand.

The upcoming tours section has a padding left that is an array.

pl: [5, 5, 5, 6, 7],

This translates to a padding left array based on the spacing key.

paddingLeft: [64, 64, 64, 128, 256]

To make sense of this array, we have to define breakpoints in the theme object.

breakpoints: ['700px', '1000px', '1200px', '1440px'],

So, what does this array translate to? If device width is less than 700 pixels, use a padding left of 64 pixels. The same padding left applies for device width ranges of (700px-1000px) and (1000px-1200px). If the device width is between 1200 pixels to 1440 pixels, use a padding left of 128 pixels. And finally, if the device width is more than 1440 pixels, use a padding left of 256 pixels.

We have a similar definition for padding right. If you calculate, padding right is padding left – 32 pixels. The difference of 32 pixels arises because of the way we define the card style. Each card has a margin right of 32 pixels.

pr: [4, 4, 4, '96px', '224px'],

The sx prop for a card is:

sx={{
  variant: 'styles.card',
  position: 'relative',
  width: [
    'calc(100% - 32px)',
    'calc(50% - 32px)',
    'calc(33.33% - 32px)',
    'calc(33.33% - 32px)',
    'calc(33.33% - 32px)',
  ],
}}

Card has a responsive width. If device width is less than 700 pixels, we stack the card vertically. For device width between 700 pixels and 1000 pixels, there are two cards in each row. And when card width is greater than 1000 pixels, there are 3 cards in each row.

Note that we have card styles in a variant. So, add that variant to the theme object.

card: {
  backgroundColor: 'background',
  height: '300px',
  mr: 4,
  mb: 4,
  padding: 3,
  borderRadius: 8,
  position: 'relative',
  textAlign: 'left',
},

Finally, there is a media query as well that needs explanation. The upcoming tours section has a h2 tag. This tag is aligned to the left. But for mobile deices, we want to centre the h2 element.

'@media screen and (max-width: 480px)': {
  textAlign: 'center',
},

We define a media query as a regular key in the sx prop.

Dark mode using theme-ui

Setting dark mode using theme-ui is very easy. We can define multiple modes within the colors key in the theme.

colors: {
  text: '#000',
  background: '#fff',
  primary: '#0552ba',
  secondary: '#ffdab9',
  highlight: '#002366',
  white: '#fff',
  white70: 'rgba(255, 255, 255, 0.7)',
  navbar: 'rgba(5, 82, 186, .7)',
  imageOverlay: 'rgba(5, 82, 186, .2)',
  hero: '#ffdab9',
  modes: {
    dark: {
      text: '#fff',
      background: '#000',
      primary: '#262629',
      secondary: '#333',
      highlight: '#1d1d1f',
      white: '#fff',
      white70: 'rgba(255, 255, 255, 0.7)',
      navbar: 'rgba(38, 38, 41, .7)',
      imageOverlay: 'rgba(0, 0, 0, .3)',
      hero: '#f2f2f2',
    },
  },
},

All that we need to do is switch the color mode to ‘dark’. To do that, we have a useColorMode hook. In App.js file import the hook.

import { useColorMode } from 'theme-ui';

At the top of the App component, use the hook like so:

const [colorMode, setColorMode] = useColorMode();

There is an icon in the nav bar to switch to dark mode. Please refer to the github repository to get the icons that we expose as React components: Darken and Brighten. Import them at the top of the App.js file.

import Brighten from './Brighten';
import Darken from './Darken';

Just before the closing head tag, add the following code:

<div sx={{ height: 24 }} onClick={handleModeChange}>
  {colorMode === 'default' ? <Darken /> : <Brighten />}
</div>;

By default, the name of the color mode is ‘default’. When the color mode is default or bright, we show the icon to switch to the dark mode. And when we are in dark mode, we show the icon to brighten or switch to default mode. Add a new handler function to make the switch.

function handleModeChange() {
  setColorMode(colorMode === 'default' ? 'dark' : 'default');
}

The handler switches the color mode to dark. When the color mode is set to dark, theme-ui uses the colors in colors.modes.dark key of the theme object.


Theme UI (theme-ui) is intuitive as we define the entire theme in a JavaScript object. For fonts and colors, we use a name to refer to the actual font or color. Setting margins and paddings is also intuitive as we set the index of the value in the space array. Defining reusable styles is possible with the help of variants. Using responsive breakpoint is a bit difficult because it involves a bit of math. Whenever we can’t use the standard breakpoint definition in the theme object, we can use a media query as a key in the sx prop. Finally, switching to dark mode is so easy.

The demo app is hosted in Netlify. And the source code (with some additions) is available in Github.

Related Posts

2 thoughts on “Style a React app using Theme UI (theme-ui) package

Leave a Reply

Your email address will not be published.