How (and Why) to Add Themes to your React App Using Styled-Components
It’s become quite common for websites and applications to provide users with a choice of visual themes. This is often just a choice between light and dark mode options. However, some applications provide additional theming, or even allow users to put together their own custom themes.
In this blog post, we’ll cover:
- Why giving users a theme choice is a good idea
- How to use the styled-components library
- How to add UI themes to a React app using React and styled-components
- How to persist theme preferences across sessions
- How to use operating system theme preferences
- How to implement user-customisable theming
By the end of this article, you should have everything you need to implement your own theming system, and a demo app to play around with. The source code for the demo app can be found on GitHub. There is also a live deployment on Northflank that you can play around with.
Why give users a choice of theme?
In the simplest terms, users have preferences. Some users might be used to reading from paper and prefer dark text on a white background. Some users might primarily use your application in the evening or at night, and want a dark background to go easy on their eyes. Whatever the reason for a user’s preference, you want to provide them with the best experience possible.
Another reason to provide a choice of themes is accessibility. Due to visual impairments, dyslexia, or a number of other conditions, some users might struggle to read text with a poor contrast ratio, or a specific background colour. Providing more than one theme option gives these users a chance to try out different visual combinations and use what works best for them. For ultimate accessibility, you can let users define their own custom themes that meet their precise needs.
At Northflank, we want our application to feel like an extension of our users’ own development environments. As well as our standard light & dark offerings, we provide a few themes that match with popular IDE colour schemes and seasonal designs, as well as letting users customise their own themes.
How to use the styled-components library
styled-components is a JavaScript styling library. It allows us to write CSS within our JS files, rather than writing separate CSS/SASS etc. This is handy for a few reasons:- Everything is in one place: now when we build a component, we can keep the component markup, functionality, and styling all in one place. No separate CSS files to keep track of or import.
- We can use JS within our styles: props passed to our styled component can be used to influence the resulting CSS.
- Styles are scoped: unlike CSS classes, which are global and affect your entire document, styled-components styles are specific to your component. This can save frustration with a polluted global class-space. Importantly, styled-components does allow us to create global styles if we really need to.
A very simple styled component might look something like this:
import styled from 'styled-components'
const Text = styled.p`
background-color: blue;
color: white;
`
<Text>Hello world!</Text>
Which would render a <p>
tag with a blue background and white foreground.
A slightly more advanced example could include a conditional style:
import styled from 'styled-components'
const Text = styled.p(
({ inverse }) => `
background-color: ${inverse ? 'white' : 'blue'};
color: ${inverse ? 'blue' : 'white'};
`
)
<Text inverse>Hello world!</Text>
Here, the background and foreground colours will switch depending on whether or not the component is passed the inverse
prop.
How to add UI themes using React and styled-components
To make styling inside an application easier, we will want to have some rules and variables that can apply to all components. For example, if you have a specific brand colour in your application, you don’t really want to be defining the hex value every time you need to use it within a component. What happens if the brand colour changes? Ideally we can define the value once and then reuse the same variable wherever needed.
To achieve this, we can put together some simple objects that will contain our theme.
src/app/themes.js
export const base = {
breakpoints: ['768px'],
space: ['0px', '2px', '4px', '8px', '16px', '32px', '64px', ...],
fonts: {
heading: 'Inter, system-ui, sans-serif',
body: 'Inter, system-ui, sans-serif',
},
fontSizes: ['12px', '14px', '16px', '20px', '24px', ...],
...
}
export const light = {
primary: '#4851f4',
background: '#ffffff',
nav: '#f8f8f8',
border: '#deebf1',
text: '#202224',
...
}
Here, base
contains some rules that will apply across all of our themes: things like spacing values, fonts, breakpoints etc. Defining these values here means that we can keep all of our styling consistent across the whole application. light
contains the colours that will make up our light theme.
Then, at the root of our application, we can make styled-components aware of this theme using a ThemeProvider
.
src/app/App.js
import React from 'react'
import { ThemeProvider } from 'styled-components'
import { base, light } from './themes'
const App = () => {
const theme = { ...base, colors: light }
return (
<ThemeProvider theme={theme}>
{/* rest of your app goes here */}
</ThemeProvider>
)
}
export default App
Now, when writing a styled component, you will always receive your theme object as a prop:
import styled from 'styled-components'
const Text = styled.p(
({ theme }) => `
color: ${theme.colors.primary};
`
)
<Text>Hello world!</Text>
Add a second theme and persist theme preference across sessions
Now that we’re using a theme in our application, it is not much extra work to add a second. Define another set of colours:
src/app/themes.js
export const base = {
breakpoints: ['768px'],
space: ['0px', '2px', '4px', '8px', '16px', '32px', '64px', ...],
fonts: {
heading: 'Inter, system-ui, sans-serif',
body: 'Inter, system-ui, sans-serif',
},
fontSizes: ['12px', '14px', '16px', '20px', '24px', ...],
...
}
export const light = {
primary: '#4851f4',
background: '#ffffff',
nav: '#f8f8f8',
border: '#deebf1',
text: '#202224',
...
}
export const dark = {
primary: '#4851f4',
background: '#1f2023',
nav: '#27282b',
border: '#303236',
text: '#f8f8f8',
...
}
Now we need to allow the user to choose a theme. We will store the current value in a React state variable.
src/app/App.js
import React, { useState } from 'react'
import { ThemeProvider } from 'styled-components'
import { base, light, dark } from './themes'
const themesMap = {
light,
dark
}
export const ThemePreferenceContext = React.createContext()
const App = () => {
const [currentTheme, setCurrentTheme] = useState('light')
const theme = { ...base, colors: themesMap[currentTheme] }
return (
<ThemePreferenceContext.Provider value={{ currentTheme, setCurrentTheme }}>
<ThemeProvider theme={theme}>
{/* rest of your app goes here */}
</ThemeProvider>
</ThemePreferenceContext.Provider>
)
}
export default App
Now from anywhere within your application, you can use the ThemePreferenceContext
context to get the setCurrentTheme
function and call it to update the current theme. Your styled components will pick up on this change and update accordingly.
For example, you could do this with a select element:
<select
value={currentTheme}
onChange={(e) => setCurrentTheme(e.target.value)}
>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
From here, hopefully you can see that it is trivial to add as many themes as you like - simply add a new set of colours, import them into your app, and make them available to be selected and stored in the state.
Storing the current theme in React state is a start, but is only part of the solution. As it stands, this preference will not persist and will be reset every time the user refreshes a page or opens your application.
Persisting the user's preference can be done in a couple of different ways:
- Save to a cookie: By saving to a cookie, the user's preference is available both to the browser and the server. This is ideal if you are doing server-side rendering and need to know the theme preference to render HTML. If you SSR without taking the user's theme preference into account, you can be left with an annoying flash when a page loads because the server is sending the ‘default’ theme which is then changed on the client when rehydration occurs.
- Save to localStorage: This is similar to using a cookie, but the value will only be available in the browser and not on the server. If you don’t need to know the theme preference on the server and everything is rendered on the client, this can be simpler.
- Save to a database: If you are already storing user accounts or profiles in a database of some sort, then it might make sense to store their theme preference as a part of their account information. This method has the added advantage of persisting a user's theme choice across different devices.
Server-side rendering
From our application, we can save the user's preference to a cookie using a library like react-cookie. Here, we also add a new prop initialTheme
to our App component, which will be important in a second.
src/app/App.js
import { useCookies } from 'react-cookie'
const App = ({ initialTheme = 'light' }) => {
const [currentTheme, setCurrentTheme] = useState(initialTheme)
const [, setCookie] = useCookies()
const handleThemeChange = (theme) => {
setCookie('themePreference', theme, {
path: '/',
expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365) // 1 year
})
}
...
}
Then, in our server, we can use the value of this cookie when we server-side render the initial HTML:
src/server/index.js
import ...
const app = express()
app.use(cookieParser())
app.use(express.static('dist'))
app.get('*', (req, res) => {
const { themePreference } = req.cookies
let app = ''
let styles = ''
const sheet = new ServerStyleSheet()
try {
app = ReactDOMServer.renderToString(
sheet.collectStyles(
<StaticRouter location={req.url}>
<App initialTheme={themePreference} />
</StaticRouter>
)
)
styles = sheet.getStyleTags()
} catch (e) {
console.error(e)
} finally {
sheet.seal()
}
const html = `
<!DOCTYPE html>
<html lang="en">
<head>
<title>react-themes-demo</title>
<script src="/app.js" async defer></script>
${styles}
</head>
<body>
<div id="root">${app}</div>
</body>
</html>`
res.send(html)
}
Here, we fetch the value of the themePreference
cookie from the request. Then, using the ReactDOMServer.renderToString
and ServerStyleSheet
from styled-components, we build the HTML and CSS content of our application. Note that we are passing our theme preference value through to the <App />
component, where it is consumed as the initial value for our React state theme variable. We then send the HTML and CSS back as the response from our request, where the user will receive a page styled with their theme preference.
For the full server source code, see the demo app repository linked at the end of the article.
How to use the operating system theme preference
Most operating systems give the user a choice between a light and a dark theme. In the browser, we can access this choice via a media query - meaning that if a user has set their OS preference to dark, we can automatically do the same in our application, giving the user a cohesive and familiar experience.
To achieve this, we can watch the prefers-color-scheme
media query, and update our state when we see a change.
src/app/App.js
import React, { useState, useEffect } from 'react'
import { ThemeProvider } from 'styled-components'
import { base, light, dark } from './themes'
const themesMap = {
light,
dark
}
export const ThemePreferenceContext = React.createContext()
const App = () => {
const [currentTheme, setCurrentTheme] = useState('light')
useEffect(() => {
const themeQuery = window.matchMedia('(prefers-color-scheme: light)')
setCurrentTheme(themeQuery.matches ? 'light' : 'dark')
themeQuery.addEventListener('change', ({ matches }) => {
setCurrentTheme(matches ? 'light' : 'dark')
})
}, [])
const theme = { ...base, colors: themesMap[currentTheme] }
return (
<ThemePreferenceContext.Provider value={{ currentTheme, setCurrentTheme }}>
<ThemeProvider theme={theme}>
{/* rest of your app goes here */}
</ThemeProvider>
</ThemePreferenceContext.Provider>
)
}
export default App
Using React’s useEffect
hook, we create a matchMedia
listener with our media query, set our theme once on first load, and then update it again every time the OS preference changes. Here, you would probably want to add some additional logic to not change the theme based on OS preference if the user has already made a theme choice inside the application.
How to implement user-customisable theming
In your application, you may also want to allow your users to create their own themes. This is more of a considered decision - you may want to only offer brand-specific themes and not allow users to deviate too far from your own meticulously designed colour schemes. Or, you may want to allow users total control, and let them change every aspect of the theme. Probably, you want to meet in the middle - you could allow users to customise a specific subset of theme colours while keeping the ones integral to your brand fixed.
To implement customisation, we can add another state variable to contain the custom theme, and conditionally apply that when the user selects ‘custom’.
src/app/App.js
import React, { useState } from 'react'
import { ThemeProvider } from 'styled-components'
import { base, light, dark } from './themes'
const themesMap = {
light,
dark
}
export const ThemePreferenceContext = React.createContext()
const App = () => {
const [currentTheme, setCurrentTheme] = useState('custom')
const [customTheme, setCustomTheme] = useState({ primary: '#4851f4', ... })
const theme = {
...base,
colors: currentTheme === 'custom' ? customTheme : themesMap[currentTheme]
}
return (
<ThemePreferenceContext.Provider
value={{
currentTheme,
setCurrentTheme,
customTheme,
setCustomTheme
}}
>
<ThemeProvider theme={theme}>
{/* rest of your app goes here */}
</ThemeProvider>
</ThemePreferenceContext.Provider>
)
}
export default App
Again, setCustomTheme
is exposed via the ThemePreferenceContext
and thus can be manipulated from anywhere in the application.
The UI to set the custom theme values could look something like this:
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
gridGap: '8px',
}}
>
{Object.entries(customTheme).map(([key, val]) => (
<label key={`custom-${key}`}>
<p>{key}</p>
<input
type="color"
value={val}
onChange={(e) => {
setCustomTheme((t) => {
const current = { ...t }
current[key] = e.target.value
return current
})
}}
style={{ display: 'block', width: '100%' }}
/>
</label>
))}
</div>
Here, we render a colour input field for each value in our theme. When a value is changed, setCustomTheme
is called, and thus the custom theme is updated in the state.
Some applications also allow users to share their custom themes with other users by providing them a simple string representation of their colour scheme. Such a system is left as an exercise for the reader.
Summary
That’s it! You should now have all of the knowledge you need to add a few themes to your React app, consume them via styled-components, and even allow your users to create their own custom themes.
Demo application
All of the ideas discussed in this blog post have been put together into a simple demo application. You can see the GitHub repository here, and an instance of the app deployed on Northflank here.
Hosting React apps on Northflank
Northflank makes building and deploying React apps from GitHub or other version control really easy. In the demo repository above, you’ll find a simple Dockerfile, which doesn’t do much more than run the build command, expose a port and run the code. All you need to do is create a service on Northflank and select your GitHub repository. Northflank will parse your Dockerfile, build your app, and deploy it to the cloud with automatic Let’s Encrypt TLS and a code.run domain name.
Check out our guide on deploying a create-react-app application on Northflank.
Northflank can build and deploy any code from version control with Dockerfiles or Buildpacks. Run all of your apps, backends, databases, and cron jobs in one place.
Thanks to Max Stoiber, creator of styled-components, for reviewing the draft of this blog post.