Theming and Theme Switching with React and styled-components
I recently had a project with a requirement to support theming on the website. It was a bit of a strange requirement, as the application is mostly used by a handful of administrators. An even bigger surprise was that they wanted not only to choose between pre-created themes, but build their own themes. I guess the people want what they want!
Let’s distill that into a complete list of more detailed requirements, then get it done!
- Define a theme (i.e. background color, font color, buttons, links, etc.)
- Create and save multiple themes
- Select and apply a theme
- Switch themes
- Customize a theme
We delivered exactly that to our client, and the last I heard, they were using it happily!
Let’s get into building exactly that. We’re going to use React and styled-components. All the source code used in the article can be found in the GitHub Repository.
The setup
Let’s set up a project with React and styled-components. To do that, we will be using the create-react-app. It gives us the environment we need to develop and test React applications quickly.
Open a command prompt and use this command to create the project:
npx create-react-app theme-builder
The last argument, theme-builder
, is just the name of the project (and thus, the folder name). You can use anything you like.
It may take a while. When done, navigate it to it in the command line with cd theme-builder
. Open the file src/App.js
file and replace the content with the following:
import React from 'react';
function App() {
return (
<h1>Theme Builder</h1>
);
}
export default App;
This is a basic React component that we will modify soon. Run the following command from the project root folder to start the app:
# Or, npm run start
yarn start
You can now access the app using the URL http://localhost:3000
.
create-react-app comes with the test file for the App component. As we will not be writing any tests for the components as part of this article, you can choose to delete that file.
We have to install a few dependencies for our app. So let’s install those while we’re at it:
# Or, npm i ...
yarn add styled-components webfontloader lodash
Here’s what we get:
- styled-components: A flexible way to style React components with CSS. It provides out-of-the-box theming support using a wrapper component called,
. This component is responsible for providing the theme to all other React components that are wrapped within it. We will see this in action in a minute.
- Web Font Loader: The Web Font Loader helps load fonts from various sources, like Google Fonts, Adobe Fonts, etc. We will use this library to load fonts when a theme is applied.
- lodash: This is a JavaScript utility library for some handy little extras.
Define a theme
This is the first of our requirements. A theme should have a certain structure to define appearance, including colors, fonts, etc. For our application, we will define each theme with these properties:
- unique identifier
- theme name
- color definitions
- fonts
You may have more properties and/or different ways to structure it, but these are the things we’re going to use for our example.
Create and save multiple themes
So, we just saw how to define a theme. Now let’s create multiple themes by adding a folder in the project at src/theme
and a file in it called, schema.json
. Here’s what we can drop in that file to establish “light” and “sea wave” themes:
{
"data" : {
"light" : {
"id": "T_001",
"name": "Light",
"colors": {
"body": "#FFFFFF",
"text": "#000000",
"button": {
"text": "#FFFFFF",
"background": "#000000"
},
"link": {
"text": "teal",
"opacity": 1
}
},
"font": "Tinos"
},
"seaWave" : {
"id": "T_007",
"name": "Sea Wave",
"colors": {
"body": "#9be7ff",
"text": "#0d47a1",
"button": {
"text": "#ffffff",
"background": "#0d47a1"
},
"link": {
"text": "#0d47a1",
"opacity": 0.8
}
},
"font": "Ubuntu"
}
}
}
The content of the schema.json
file can be saved to a database so we can persist all the themes along with the theme selection. For now, we will simply store it in the browser’s localStorage
. To do that, we’ll create another folder at src/utils
with a new file in it called, storage.js
. We only need a few lines of code in there to set up localStorage
:
export const setToLS = (key, value) => {
window.localStorage.setItem(key, JSON.stringify(value));
}
export const getFromLS = key => {
const value = window.localStorage.getItem(key);
if (value) {
return JSON.parse(value);
}
}
These are simple utility functions to store data to the browser’s localStorage
and to retrieve from there. Now we will load the themes into the browser’s localStorage
when the app comes up for the first time. To do that, open the index.js
file and replace the content with the following,
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import * as themes from './theme/schema.json';
import { setToLS } from './utils/storage';
const Index = () => {
setToLS('all-themes', themes.default);
return(
<App />
)
}
ReactDOM.render(
<Index />
document.getElementById('root')
);
Here, we are getting the theme information from the schema.json
file and adding it to the localStorage
using the key all-themes
. If you have stopped the app running, please start it again and access the UI. You can use DevTools in the browser to see the themes are loaded into localStorage
.
Select and apply a theme
We can now use the theme structure and supply the theme object to the wrapper.
First, we will create a custom React hook. This will manage the selected theme, knowing if a theme is loaded correctly or has any issues. Let’s start with a new useTheme.js
file inside the src/theme
folder with this in it:
import { useEffect, useState } from 'react';
import { setToLS, getFromLS } from '../utils/storage';
import _ from 'lodash';
export const useTheme = () => {
const themes = getFromLS('all-themes');
const [theme, setTheme] = useState(themes.data.light);
const [themeLoaded, setThemeLoaded] = useState(false);
const setMode = mode => {
setToLS('theme', mode)
setTheme(mode);
};
const getFonts = () => {
const allFonts = _.values(_.mapValues(themes.data, 'font'));
return allFonts;
}
useEffect(() =>{
const localTheme = getFromLS('theme');
localTheme ? setTheme(localTheme) : setTheme(themes.data.light);
setThemeLoaded(true);
}, []);
return { theme, themeLoaded, setMode, getFonts };
};
This custom React hook returns the selected theme from localStorage
and a boolean to indicate if the theme is loaded correctly from storage. It also exposes a function, setMode
, to apply a theme programmatically. We will come back to that in a bit. With this, we also get a list of fonts that we can load later using a web font loader.
It would be a good idea to use global styles to control things, like the site’s background color, font, button, etc. styled-components provides a component called, createGlobalStyle
that establishes theme-aware global components. Let’s set those up in a file called, GlobalStyles.js
in the src/theme
folder with the following code:
import { createGlobalStyle} from "styled-components";
export const GlobalStyles = createGlobalStyle`
body {
background: ${({ theme }) => theme.colors.body};
color: ${({ theme }) => theme.colors.text};
font-family: ${({ theme }) => theme.font};
transition: all 0.50s linear;
}
a {
color: ${({ theme }) => theme.colors.link.text};
cursor: pointer;
}
button {
border: 0;
display: inline-block;
padding: 12px 24px;
font-size: 14px;
border-radius: 4px;
margin-top: 5px;
cursor: pointer;
background-color: #1064EA;
color: #FFFFFF;
font-family: ${({ theme }) => theme.font};
}
button.btn {
background-color: ${({ theme }) => theme.colors.button.background};
color: ${({ theme }) => theme.colors.button.text};
}
`;
Just some CSS for the , links and buttons, right? We can use these in the
App.js
file to see the theme in action by replace the content in it with this:
// 1: Import
import React, { useState, useEffect } from 'react';
import styled, { ThemeProvider } from "styled-components";
import WebFont from 'webfontloader';
import { GlobalStyles } from './theme/GlobalStyles';
import {useTheme} from './theme/useTheme';
// 2: Create a cotainer
const Container = styled.div`
margin: 5px auto 5px auto;
`;
function App() {
// 3: Get the selected theme, font list, etc.
const {theme, themeLoaded, getFonts} = useTheme();
const [selectedTheme, setSelectedTheme] = useState(theme);
useEffect(() => {
setSelectedTheme(theme);
}, [themeLoaded]);
// 4: Load all the fonts
useEffect(() => {
WebFont.load({
google: {
families: getFonts()
}
});
});
// 5: Render if the theme is loaded.
return (
<>
{
themeLoaded && <ThemeProvider theme={ selectedTheme }>
<GlobalStyles/>
<Container style={{fontFamily: selectedTheme.font}}>
<h1>Theme Builder</h1>
<p>
This is a theming system with a Theme Switcher and Theme Builder.
Do you want to see the source code? <a href="https://github.com/atapas/theme-builder" target="_blank">Click here.</a>
</p>
</Container>
</ThemeProvider>
}
</>
);
}
export default App;
A few things are happening here:
- We import the
useState
anduseEffect
React hooks which will help us to keep track of any of the state variables and their changes due to any side effects. We importThemeProvider
andstyled
from styled-components. TheWebFont
is also imported to load fonts. We also import the custom theme,useTheme
, and the global style component,GlobalStyles
. - We create a
Container
component using the CSS styles andstyled
component. - We declare the state variables and look out for the changes.
- We load all the fonts that are required by the app.
- We render a bunch of text and a link. But notice that we are wrapping the entire content with the
wrapper which takes the selected theme as a prop. We also pass in the
component.
Refresh the app and we should see the default “light” theme enabled.
We should probably see if switching themes works. So, let’s open the useTheme.js
file and change this line:
localTheme ? setTheme(localTheme) : setTheme(themes.data.light);
…to:
localTheme ? setTheme(localTheme) : setTheme(themes.data.seaWave);
Refresh the app again and hopefully we see the “sea wave” theme in action.
Switch themes
Great! We are able to correctly apply themes. How about creating a way to switch themes just with the click of a button? Of course we can do that! We can also provide some sort of theme preview as well.
Let’s call each of these boxes a ThemeCard
, and set them up in a way they can take its theme definition as a prop. We’ll go over all the themes, loop through them, and populate each one as a ThemeCard
component.
{
themes.length > 0 &&
themes.map(theme =>(
<ThemeCard theme={data[theme]} key={data[theme].id} />
))
}
Now let’s turn to the markup for a ThemeCard
. Yours may look different, but notice how we extract its own color and font properties, then apply them:
const ThemeCard = props => {
return(
<Wrapper
style={{backgroundColor: `${data[_.camelCase(props.theme.name)].colors.body}`, color: `${data[_.camelCase(props.theme.name)].colors.text}`, fontFamily: `${data[_.camelCase(props.theme.name)].font}`}}>
<span>Click on the button to set this theme</span>
<ThemedButton
onClick={ (theme) => themeSwitcher(props.theme) }
style={{backgroundColor: `${data[_.camelCase(props.theme.name)].colors.button.background}`, color: `${data[_.camelCase(props.theme.name)].colors.button.text}`, fontFamily: `${data[_.camelCase(props.theme.name)].font}`}}>
{props.theme.name}
</ThemedButton>
</Wrapper>
)
}
Next up, let’s create a file called ThemeSelector.js
in our the src
folder. Copy the content from here and drop it into the file to establish our theme switcher, which we need to import in App.js
:
import ThemeSelector from './ThemeSelector';
Now we can use it inside the Container
component:
<Container style={{fontFamily: selectedTheme.font}}>
// same as before
<ThemeSelector setter={ setSelectedTheme } />
</Container>
Let’s refresh the browser now and see how switching themes works.
The fun part is, you can add as many as themes in the schema.json
file to load them in the UI and switch. Check out this schema.json
file for some more themes. Please note, we are also saving the applied theme information in localStorage
, so the selection will be retained when you reopen the app next time.
Customize a theme
Maybe your users like some aspects of one theme and some aspects of another. Why make them choose between them when they can give them the ability to define the theme props themselves! We can create a simple user interface that allows users to select the appearance options they want, and even save their preferences.
We will not cover the theme creation code explanation in details but, it should be easy by following the code in the GitHub Repo. The main source file is CreateThemeContent.js
and it is used by App.js
. We create the new theme object by gathering the value from each input element change event and add the object to the collection of theme objects. That’s all.
Before we end…
Thank you for reading! I hope you find what we covered here useful for something you’re working on. Theming systems are fun! In fact, CSS custom properties are making that more and more a thing. For example, check out this approach for color from Dieter Raber and this roundup from Chris. There’s also this setup from Michelle Barker that relies on custom properties used with Tailwind CSS. Here’s yet another way from Andrés Galente.
Where all of these are great example for creating themes, I hope this article helps take that concept to the next level by storing properties, easily switching between themes, giving users a way to customize a theme, and saving those preferences.
Let’s connect! You can DM me on Twitter with comments, or feel free to follow.
The post Theming and Theme Switching with React and styled-components appeared first on CSS-Tricks.
You can support CSS-Tricks by being an MVP Supporter.