← Back to Blog
Header image for blog post: Adding themes to a React app using styled-components

Adding themes to a React app using styled-components

By Tom Snelling

Published 25th January 2022

It is common for websites and web applications to provide users with a choice of visual themes. Often this is just a choice between a light and dark options, but some applications provide multiple options or even let users put together their own custom themes.

In this blog post, we’ll go through some reasons you might want to provide theme options in your next application, and have a look how this can be achieved using React and styled-components. By the end of the article, you should have everything you need to implement your own theming system and have 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 theme choice?

In the most simple 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 time and want a dark background to go easier on their eyes. Whatever the reason for their preference may be, you want to provide your users 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 with reading text with a poor contrast ratio or reading text against a specific background colour. Providing more than one theme option gives these users a chance to try out different themes and see what works best for them. For ultimate accessibility, it can be good to let users define their own themes that meet their exact needs.

At Northflank, we want our application to feel like an extension of our users’ own development environment. As well as our standard light & dark offerings, we provide a few themes that match with popular IDE colour schemes, as well as letting users customise their own themes.

Frame 259.png

Introduction to styled-components

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.

Using a theme with 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>

Adding a second theme and allowing the user to switch

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 users 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 users 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.

Using 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.

Frame 258.png

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.

Allowing users to create their own themes

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.

Screenshot 2022-01-20 at 10.47.49.png

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

Screenshot 2022-01-20 at 10.45.41.png

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.

Get started with Northflank in minutes. Our generous free tier gives you 2 services, 2 cron jobs and a database to get you going.

Thanks to Max Stoiber, creator of styled-components, for reviewing the draft of this blog post.

Share this article with your network