Article Logo Go Back

Dynamic Theming with Styled Components and NextJS

AvatarJul 10, 201915 minsTim Ellenberger

🔥 Demo First 🔥

Choose a new page theme

When building themable React apps, I have three primary concerns:

  • The entire app has a theme.
  • A single page can have a theme.
  • The page or app theme can change at runtime.

Concerns #1 and #2 are represented on this page right now. By clicking through the links of this blog, you can see that every page has it's own unique set of theme variables, such as background and font colors. Variables optionally override the default app theme.

The venerable CSS-in-JS library styled-components comes with a <ThemeProvider theme={theme}/> component which uses React Context to pass it's theme variables to any of it's child components.

In a Next.js app, it's easy to apply this <ThemeProvider /> to all pages by wrapping <Component /> in /pages/_app.js.

pages_app.jsjsx
1render() {2  const { Component, pageProps } = this.props;3  const appTheme = {4    fontColor: 'black',5    backgroundColor: 'white'6  };78  return (9    <Container>10      <ThemeProvider theme={appTheme}>11        <Component {...pageProps} />12      </ThemeProvider>13    </Container>14  );15}

Since all pages are now descendants of <ThemeProvider />, any component of these pages has easy access to fontColor and backgroundColor.

pagescool-page.jsjsx
1import styled from 'styled-components';23const Page = () => (4  <StyledPage>5    I'm a themed page!6  </StyledPage>7);89export default Page;1011const StyledPage = styled.div`12  background-color: {({ theme }) => theme.backgroundColor };13  color: {({ theme }) => theme.fontColor };14`;

In Next.js, static properties of a page can be accessed in /pages/_app.js. Let's add a static property to the Page called pageTheme

pagescool-page.jsjsx
1import styled from 'styled-components';23const Page = () => (4  <StyledPage>5    I'm a themed page!6  </StyledPage>7);89Page.pageTheme = {10  backgroundColor: green;11  fontColor: purple;12};1314export default Page;1516const StyledPage = styled.div`17  background-color: {({ theme }) => theme.backgroundColor };18  color: {({ theme }) => theme.fontColor };19`;

Then we'll merge our page theme variables into the default app theme.

pages_app.jsjsx
1render() {2  const { Component, pageProps } = this.props;3  const { pageTheme } = Component;45  const theme = {6    // Default app theme7    ...appTheme,8    // Any theme variables provided by the page9    ...pageTheme10  };1112  return (13    <Container>14      <ThemeProvider theme={theme}>15        <Component {...pageProps} />16      </ThemeProvider>17    </Container>18  );19}

The Dynamic Page Theme

In order to change our page theme dynamically, i.e. at the push of a button, we'll need to transcend our static page properties with actual state changes inside /pages/_app.js.

The overall goal is to maintain state in /pages/_app.js with a list of pages and their dynamic page overrides. We'll then need to create a function for retrieving and updating the current page's dynamic theme variables. The updateTheme() function will be passed as a prop to our page which can be used to update the theme!

pages_app.jsjsx
1/**2 * Maintain a list of dynamic theme variables for each page3 * 4 * dynamicPageThemes: [5 *    {6 *      route: '/cool-page',7 *      dynamicTheme: {8 *        backgroundColor: 'grey',9 *        fontColor: 'blue'10 *      }11 *    }12 * ]13 */14state = {15  dynamicPageThemes: []16};1718/**19 * Updates the current page's theme with provided variables20 *21 * @param dynamicTheme object22 */23updateTheme = dynamicTheme => {24  // Get the current page route i.e. /cool-page25  const { route } = this.props.router;26  const { dynamicPageThemes } = this.state;2728  // Lookup this page in state, create or update if necessary29  const pageIndex = dynamicPageThemes.findIndex(page => page.route === route);30  if (pageIndex === -1) dynamicPageThemes.push({ route, dynamicTheme });31  else dynamicPageThemes[pageIndex] = { route, dynamicTheme };3233  // Add dynamic theme vars to state34  this.setState({ dynamicPageThemes });35};3637/**38 * Retrieves any dynamic theme vars for current page39 *40 * @returns object41 */42getDynamicPageTheme = () => {43  // Get the current page route i.e. /cool-page44  const { route } = this.props.router;45  const { dynamicPageThemes } = this.state;4647  // Lookup this page in state if it exists48  const dynamicPageTheme = dynamicPageThemes.find(49    pageTheme => pageTheme.route === route50  );5152  // Return any dynamic theme variables for the current page route53  return dynamicPageTheme ? dynamicPageTheme.dynamicTheme : {};54};5556render() {57  const { Component, pageProps } = this.props;58  const { pageTheme } = Component;59  const dynamicTheme = this.getDynamicPageTheme();6061  const theme = {62    // Default app theme63    ...appTheme,64    // Any theme variables provided by the page65    ...pageTheme,66    // Override any static page variables with dynamically set variables67    ...dynamicTheme68  };6970  return (71    <Container>72      <ThemeProvider theme={theme}>73        <Component {...pageProps} updateTheme={this.updateTheme} />74      </ThemeProvider>75    </Container>76  );77}

Since our theme that is passed to ThemeProvider is now a product of state changes, we can dynamically update any page by calling this.props.updateTheme()

pagescool-page.jsjsx
1import PropTypes from 'prop-types';2import styled from 'styled-components';34const Page = ({ updateTheme }) => (5  <StyledPage>6    I'm a themed page!7    <button8      type="button"9      onClick={() =>10        updateTheme({ backgroundColor: 'magenta', fontColor: 'grey' })11      }12    >13      Grey Theme14    </button>15  </StyledPage>16);1718Page.pageTheme = {19  backgroundColor: green;20  fontColor: purple;21};2223BlogPage.propTypes = {24  updateTheme: PropTypes.func.isRequired25};2627export default Page;2829const StyledPage = styled.div`30  background-color: {({ theme }) => theme.backgroundColor };31  color: {({ theme }) => theme.fontColor };32`;

The Random Button

There is a treasure trove of cool color palettes over at colourlovers.com.

Conveniently, there is a library for exactly this purpose on npm!

In order to automatically generate a complete page theme from random color palettes, there is only one hardfast rule: The contrast of the text to background must be high enough that the page is legible.

The basic algorithm is:

  • Choose a random colourlovers.com color palette
  • Pick the first color in the palette as the background color.
  • Find the top two highest contrast colors in the palette against the background color.
  • If these contrasts exceed our CONTRAST_THRESHOLD, then these colors will be used to update our theme.
  • If these contrasts DO NOT exceed our CONTRAST_THRESHOLD, set the next color in the palette as our background color and try again.
  • If we've gone through our entire color palette and still haven't found a suitable combination to meet our CONTRAST_THRESHOLD, pick a new random palette and try again until we've found something decent.
pagescool-page.jsjsx
1import colors from 'nice-color-palettes/500';2import bestContrast from 'get-best-contrast-color';3import getContrastRatio from 'get-contrast-ratio';45/**6 * Picks a random top-rated color palette from7 * https://www.colourlovers.com/ to generate a page theme.8 *9 * https://github.com/Jam3/nice-color-palettes10 */11const generateColorPalette = () => {12  // Font and Highlight Font contrast must equal or exceed13  // this value against background color14  const CONTRAST_THRESHOLD = 4.5;1516  let backgroundColor;17  let fontColor;18  let highlightFontColor;1920  // Returns true if background-font contrast is above21  // CONTRAST_THRESHOLD, otherwise false22  const goodBackgroundContrast = () => {23    if (24      getContrastRatio(backgroundColor, fontColor) >=25        CONTRAST_THRESHOLD &&26      getContrastRatio(backgroundColor, highlightFontColor) >=27        CONTRAST_THRESHOLD28    )29      return true;3031    return false;32  };3334  // Find color palette with good contrast35  do {36    // Choose random color palette37    const palette =38      colors[Math.floor(Math.random() * Math.floor(colors.length))];3940    // Find good background/font colors within palette41    // eslint-disable-next-line no-restricted-syntax42    for (const currBackground of palette) {43      // Set theme colors based on current background of palette44      backgroundColor = currBackground;45      fontColor = bestContrast(currBackground, palette);46      highlightFontColor = bestContrast(47        currBackground,48        // eslint-disable-next-line no-loop-func49        palette.filter(color => color !== fontColor)50      );5152      // Use current palette colors if they meet contrast threshold53      if (goodBackgroundContrast()) break;54    }55  } while (!goodBackgroundContrast());5657  return {58    fontColor,59    highlightFontColor,60    backgroundColor61  };62};

Usage is as simple as calling generateColorPalette() and passing it's result to updateTheme().

pagescool-page.jsjsx
1import PropTypes from 'prop-types';2import styled from 'styled-components';34const Page = ({ updateTheme }) => (5  <StyledPage>6    I'm a themed page!7    <button8      type="button"9      onClick={() => updateTheme(generateColorPalette())}10    >11      Grey Theme12    </button>13  </StyledPage>14);1516Page.pageTheme = {17  backgroundColor: green;18  fontColor: purple;19};2021BlogPage.propTypes = {22  updateTheme: PropTypes.func.isRequired23};2425export default Page;2627const StyledPage = styled.div`28  background-color: {({ theme }) => theme.backgroundColor };29  color: {({ theme }) => theme.fontColor };30`;

Check out the full demo on CodeSandbox


Need a developer? Drop me a line!

Bug?
Edit Post