React theming with emotion

What we'll build

A day / night theme switcher based on Google Creative Lab's Night and Day project.

Packages required

This code sample is based on create-react-app.
Once you have the base project scaffolded, install the following packages:

  • emotion for the base css in js functionality
  • react-emotion for generating "styled components"
  • emotion-themable for theming support
npm install emotion react-emotion emotion-themable

Emotion also supports a babel preset that will do some of the transformations at build time.
This will make the final bundle size smaller, however it does not work with create-react-app so I decided not to use it here.
You can read more about it in the Emotion 8 launch blog post.

High level picture

For this page we will need 4 components:

  • the Sky for the background
  • the CelestialObject for the sun or the moon
  • a Title to show some instructions
  • an App to put it all together

The root component will basically be just:

<Sky>
    <Title>{this.state.title}</Title>
    <CelestialObject
        onClick={() => this.handleClick()}>
    </CelestialObject>
</Sky>

When the user will click the big circle, we will pass in a different theme to the components.

Applying the theme

There are two ways to pass a theme:

  • Manually, using the theme prop
<Sky theme={dayTheme}/>
  • Using a ThemeProvider
<ThemeProvider theme={dayTheme}>
    <Sky/>
</ThemeProvider>

We will use the ThemeProvider which will make the theme property available to all components in our React app.

App component

The main App component will handle several things

  • keeps the state of what theme is currently active
  • toggles the state when the sun/moon is clicked
  • uses the ThemeProvider to make sure the theme is passed to all children
import React from 'react';
import { render } from 'react-dom';
import { ThemeProvider } from 'emotion-theming';

import './globalStyles';
import Sky from './Sky';
import CelestialObject from './CelestialObject';
import Title from './Title';


// Define our themes: one for day and one for night
const dayTheme = {
  skyColor: '#37d8e6',
  celestialObjectColor: '#ffdd00',
  celestialObjectBorderColor: '#f1c40f'
};

const nightTheme = {
  skyColor: '#2c3e50',
  celestialObjectColor: '#bdc3c7',
  celestialObjectBorderColor: '#eaeff2'
}

// Main app
class App extends React.Component {
  constructor(props) {
    super(props);

    // Initial state: day time!
    this.state = {
      isDay: true,
      theme: dayTheme,
      title: 'Click the Sun to switch the theme'
    };
  }

  handleClick() {
    // Toggle day / night on click
    const isDay = !this.state.isDay;

    this.setState({
      isDay: isDay,
      theme: isDay ? dayTheme : nightTheme,
      title: isDay ? 'Now click the Sun' : 'Now click the Moon'
    });
  }

  render() {
    // Wrap the entire content in a <ThemeProvider>
    return <ThemeProvider theme={this.state.theme}>
        <Sky>
          <Title>{this.state.title}</Title>
          <CelestialObject
            onClick={() => this.handleClick()}>
          </CelestialObject>
        </Sky>
    </ThemeProvider>
  }
}


render(<App />, document.getElementById('root'));

Sky component

This basically just shows a light blue or dark blue background, based on the active theme.

This is almost identical to styled-components syntax, however with emotion you can also choose to use object literals if you prefer (like glamorous does).

Notice how we use styled('div') instead of styled.div. This is only becasue we don't have the babel preset active. If we did, styled.div would also be supported.

import styled from 'react-emotion'

const Sky = styled("div")`
  height: 100%;
  width: 100%;
  background-color: ${props => props.theme.skyColor}
`;

export default Sky;

CelestialObject component

This is our Sun  and Moon! Actually, it's just a circle whose color and border change based on the theme.

Also, on hover the border width changes.

Emotion support SCSS-style nesting, as you can see with the :hover attribute used below.

import styled from 'react-emotion'

const CelestialObject = styled("div")`
  height: 250px;
  width: 250px;
  border-radius: 100%;
  padding: 20px;
  margin: auto;
  position: absolute;
  top: 0;
  left: 0;
  bottom: 0;
  right: 0;
  background-color: ${props => props.theme.celestialObjectColor};
  border: 10px solid ${props => props.theme.celestialObjectBorderColor};

  &:hover {
    border: 20px solid ${props => props.theme.celestialObjectBorderColor};
  }
`;

export default CelestialObject;

Global styles

To make sure the sky fills the whole page, we need to set the width and height of the body to 100%.

This is a great use case for applying global styles.

import { injectGlobal } from 'react-emotion';

injectGlobal`
  html, body, #root {
    width: 100%;
    height: 100%;
    padding: 0;
    margin: 0; 
  }
`

That's it!

You can edit with this code live on CodeSandbox: