Theming in React with Styled Components

Get started using Styled Theming on top of Styled Components for multi-dimensional CSS theming

Styled Components aims to solve the problem of applying CSS to Javascript frameworks on a per-component basis; and this is particularly useful with React. Although this article is primarily aimed at theming with the ecosystem of tools built around Styled Components, it can also serve as an introductory to the package and its capabilities.

The package itself is continuing an upwards trend of adoption, currently on 736k weekly downloads at publish time. It is clear that embedded styles will play an important role in the future of Javascript frameworks, having a number of benefits over stylesheets.

One of the more advanced use cases of the package is theming — allowing your app to support multiple looks and feels. Adopting a light and dark mode will involve theming your app, as would integrating a compact vs cozy layout. We will explore how to do this at scale, along with the tools needed for the job. As well as styled-components, we will also utilise styled-theming; a small package designed specifically for defining and managing themes on top of styled-components.

Setting up Styled Components for Themes

This section documents the critical tools needed to efficiently use styled components.

As you may know, styled components utilise template literals as a means of defining styles directly into your component files:

// MyComponent.jsxconst Wrapper = styled.div`
display: flex;
padding: 1rem;
`;
class MyComponent extends React.Component {
...

Having full-blown styles directly in Javascript is powerful, but by default most IDEs do not recognise that we intend this string to represent CSS. Luckily the community have steadily been bringing support to the major IDEs:

  • Atom: Install the language-babel package and restart Atom for syntax highlighting to take effect.
  • Sublime Text: The best option right now is Naomi, supplying enhanced syntax highlighting for the editor. git clone the package directly into your Packages/ folder and restart Sublime. You can then find Naomi syntax options under View -> Syntax.
  • Visual Studio Code: Install Language Babel from the Visual Studio Marketplace.

Check out a full list of syntax highlighting options here.

We have a couple of options for supporting styled-components within your React project. You can indeed install the package as a dependency:

yarn add styled-components

From here we can import functions like we would any other package:

import styled from 'styled-components'

However, if you use Create React App where support for babel macros has been available since version 2, we also have the option of installing styled-components as a babel macro too:

yarn add styled-components.macro --dev

And import functions from the macro like so:

import styled from 'styled-components/macro'

Note: babel-plugin-macros defines a standard interface for libraries that want to use compile-time code transformation without requiring the user to add a babel plugin to their build system. This speeds up compile time and makes managing babel plugins more streamlined. The feature has been picking up steam recently, on around 2M weekly downloads at the time of writing.

styled-theming gives us a means to more effectively manage themes as they grow (this is the package in its entirety; ~300bytes minified). Install it with the following:

yarn add styled-theming

Let’s next explore styled-theming and the benefits it brings to the table in conjunction with styled-components.

Exploring Styled Theming

Why use styled-theming on top of styled-components beyond just taking my word for it? It gives us a means to effectively scale themes as they grow in complexity by defining names instead of styles. Consider the following example, defining a couple of properties for a dark and light mode:

...
import theme from 'styled-theming';
// define background colours for `mode` themeconst backgroundColor = theme('mode', {
light: '#fafafa',
dark: '#222'
});
// define text color for `mode` themeconst textColor = theme('mode', {
light: '#000',
dark: '#fff'
});
// apply theming to a styled componentconst Wrapper = styled.div`
background-color: ${backgroundColor};
color: ${textColor}
`;
// use styled-components as we would expect in render(), housing <Wrapper /> within a <ThemeProvider />export default function App() {
return (
<ThemeProvider theme={{ mode: 'light' }}>
<Wrapper>
Hello World
</Wrapper>
</ThemeProvider>
);
}

As you can see, styled-theming allows us to compartmentalise theme properties. Concretely:

  • We are not limited to defining every theme option in one file. As demonstrated, we are using the theme() utility from styled-theming, that can be used anywhere in your component hierarchy. It is an accepted development style to create component-specific theming within that component’s file, rather than defining the theme in its entirety at the root component.
  • The same point regarding the component tree can be applied to our Wrapper component: Styled components are also defined within their respected component file. This is the beauty of using the package; defining styles where they are used as opposed to importing a stylesheet that a multitude of components may be using, decreasing the complexity of maintaining styles.
  • Within <ThemeProvider /> we provide a value for the mode theme. mode can be referred to as a theme “name”.

We are not limited to only one theme name — we could define an additional name, let’s say, layout, that determines how compact the app content is:

...
const padding = theme('layout', {
compact: '0.5rem',
cozy: '1.5rem'
});
...
<ThemeProvider theme={{ mode: 'light', layout: 'cozy' }}>
...

The power here lies in the ability for multiple themes to be used together. For example, I may want a compact dark mode, or a cozy light mode, that utilise two separate themes concurrently. Having the ability to manage this is pretty convenient as a developer, and a pretty cool experience for an end-user.

Along with theme(), styled-theming also gives us another utility: theme.variants(). Wouldn’t it be nice if we could have a concise way to define a range of styles for a component based on props? I could then tailor my button styles by simply passing props, like <Button small /> or <Button medium /> — that will also be fully compatible with my themes.

This is the problem theme.variants() aim to solve.

As an example of using variants, we may have many button styles to contend with: default, inverse, large, small, etc. We can provide specific styles for these variations using the appropriately termed “theme variants”.

Here is an example that takes a kind prop that then determines a font-size for a button, depending on the given layout theme:

// defining our `layout` theme properties based on `kind`const buttonFontSize = theme.variants('layout', 'kind', {
small: { compact: '0.9rem', cozy: '1.2rem' },
medium: { compact: '1rem', cozy: '1.3rem' },
large: { compact: '1.1rem', cozy: '1.4rem' },
});
// defining our Button styled componentconst Button = styled.button`
font-size: ${buttonFontSize};
`;
// configure propTypes and defaultProps for ButtonButton.propTypes = {
kind: PropTypes.oneOf(['small', 'medium', 'large']),
};
Button.defaultProps = {
kind: 'medium',
};
...// render render() {
return (
<ThemeProvider theme={{ layout: 'compact' }}>
<Button large>
Sign in
</Button>
</ThemeProvider>
);
}

We have given our Button component prop types and default props, of which kind will determine the font-size taken from our theme variants.

Ideas may now be coming into your mind with ways you can apply these tools — light and dark, cozy and compact are just a couple of ways we can coherently manipulate CSS based on a theme.

Here are a few more use cases:

  • Accessibility: Do you wish to provide large fonts and a more grey-scale oriented design for visually impaired or colour blind users?
  • User customisation: Allow your users to customise their profile page with a main colour that then determines other shades that compliment the UI. A good example of this are Twitter’s customisable user profile options.
  • Compatibility: If your audience primarily uses legacy hardware, you could provide a compatibility theme with a limited and fully-featured theme; fully-featured providing transitions, keyframe animations and other cutting-edge CSS features, while a limited theme providing no transitions, transforms, text gradients, etc.
  • Time of day: You could utilise MomentJS and the user’s local time to determine the time of day, and reflect this with a theme suited for that time: perhaps a sunrise, daytime, dusk and night themes would provide some nice UX customisation.

As demonstrated above, your themes must exist within a <ThemeProvider /> component that defines which theme is currently in use by child components. Theme Providers are contextually defined — in other words, we can overwrite a ThemeProvider further down our component tree:

<ThemeProvider theme={{ mode: 'light' }}>
<SideBar />
<ThemeProvider theme={{ mode: 'dark' }}>
<Content>
</ThemeProvider>
</ThemeProvider>

In the above JSX, my <SideBar /> will be inheriting light theme styles, whereas my main <Content /> will be inheriting dark theme styles.

This can be useful for the profile page customisation use case, where you may wish to keep other sections of the app with a default style, like your legal, press, or jobs sections.

Theming with Styled Components

Now, with the understanding of styled-theming, let’s visit some important features of styled-components that we can utilise in relation to theming, and understand the differences between the two.

styled-components have a section on their Advanced usage page that documents how to theme your styles without the addition of the styled-theming package.

This documentation should not be confused with styled-theming.

The section does however demonstrate how we can use props to pass theme styles through the <ThemeProvider />, in the form of the following syntax (Can you see how the usage differs from styled-theming?):

// Define the `theme` propconst theme = {
main: {
color: 'black'
},
...
};
// Button styled-component with props.theme propertiesconst Button = styled.button`
color: ${props => props.theme.main.color};
border: 2px solid ${props => props.theme.main.color};
`;
// provide default theme properties for ButtonButton.defaultProps = {
theme: {
main: {
color: 'white'
}
}
}
render(
<>
<Button>Un-themed Button</Button>
<ThemeProvider theme={theme}>
<Button>Themed Button</Button>
</ThemeProvider>
</>
);

The above example attempts to structure a vanilla styled-component theme solution in a way we would expect coming from styled-theming. But there is one key difference in the way we are providing theme values — let’s examine this difference.

So it is now clear that styled-components and styled-theming solve the theme problem in different ways:

  • styled-components pass theme styles down a Theme Provider.
  • styled-theming pass theme names down a Theme Provider.

Unlike styled-theming, we do not have the convenience of theme() and theme.variants(), that coherently allow us to define theme properties within any component based on the theme name. Instead, we are relying on props to pass our theme styles down components via the Theme Provider.

Note: Styled Components provide a withTheme() HOC that injects a theme prop into non-styled React components. Although your theme is primarily used for styling purposes, it can also be referenced for other logic within your components. I do not deem this feature overly useful, but it is there nonetheless if you deem it necessary for a use case — perhaps to determine an image for each theme.

Passing styles directly down from our root component may appear limiting. styled-components attempt to overcome this with function themes, allowing us to take a theme from a parent Theme Provider and redefining it to be injected into a child Theme Provider.

This is useful for amending themes as you go further down your component tree, but its use cases are limiting — as a designer you will want to keep your theming consistent throughout the app, and therefore refrain from too many changes.

Media Templates

Although not strictly theming, media templates are another useful feature that we can take advantage of to optimise the syntax when defining media queries. Some might view this as a type of theming, whereby styling for various media sizes represents a theme.

The following example demonstrates how to define a media template to handle styles for various screen sizes:

// configure screen width thresholdsconst ScreenSizes = {
DESKTOP: 992,
TABLET: 768,
PHONE: 576
}
const sizes = {
desktop: ScreenSizes.DESKTOP,
tablet: ScreenSizes.TABLET,
phone: ScreenSizes.PHONE
}
// iterate through sizes and create a media templateconst media =
Object
.keys(sizes)
.reduce((acc, label) => {
acc[label] = (...args) => css`
@media (max-width: ${sizes[label] / 16}em) {
${css(...args)}
}
`

return acc
}, {});
// use media methods with styled-component instead of raw @media queriesconst StyledContent = styled.div`
width: 100%
${media.desktop`
padding: 10px;
`}
${media.tablet`
padding: 15px;
max-width: 700px;
`}
${media.phone`
padding: 20px;
max-width: 900px;
`}
`;
export class Content extends React.Component { render() {
return (
<StyledContent>
{ this.children }
</StyledContent>
);
}
}

The above example utilises media templates to adjust the component padding and maximum content width depending on the screen size; a compact mobile design will require less padding and the full screen width, whereas a desktop screen will require a maximum content width and centered content.

There is no mention of how to pass your media object through components in the styled-components documentation, but you will undoubtedly want to take advantage of it with styled-theming when defining styles on a per-component basis. Although you could embed media into <ThemeProvider /> as another theme name, you may wish to set up your own Context Provider to pass it down, keeping the utility separate from your themes.

Where to go From Here

Styled Components is now adopted as a critical tool for large scale apps, and will not be going away anytime soon. If only for restructuring your CSS without theming, the package may be a great step forward for making your code more maintainable. Not having to manage class names in and of itself will eliminate the problem of unused styles, while keeping what you have defined down to a minimum.

There are many features we have not touched on here, including support for Typescript, Jest testing, and more — in fact the sheer amount of features and support for styled-components is what makes the package very capable for both small teams and large organisations.

For raw documentation, start off with the documentation home page.

Continue reading my next article to learn how to use styled-components and styled-theming to toggle between a dark mode in your React apps:

Programmer and Author. Director @ JKRBInvestments.com. Creator of LearnChineseGrammar.com for iOS.

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store