React theming with styled components

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 styled-components package:

npm install styled-components

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
  • Using a ThemeProvider
<Sky theme={dayTheme}/>
<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 'styled-components';

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.

Things to notice:

  • we can access skyColor from the theme simply by reading props.theme.skyColor
  • the syntax is just plain CSS, mixed in with some javascript (see template literals)
import styled from 'styled-components';

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.

This is achieved using the SCSS-style nested &:hover selector.

import styled from 'styled-components';

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.

This is achieved using the injectGlobal helper.

import { injectGlobal } from 'styled-components';

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

That's it!

You can edit with this code live on CodeSandbox: