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 functionalityreact-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 whattheme
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: