💾 Archived View for wilw.capsule.town › log › 2021-08-21-react-theming.gmi captured on 2024-08-18 at 17:18:36. Gemini links have been rewritten to link to archived content

View Raw

More Information

⬅️ Previous capture (2023-04-19)

-=-=-=-=-=-=-

🏡 Home

Back to gemlog

Adding 'dark mode' and dynamic theming to your React websites

Posted on 21 August 2021

Adding theming and the choice between "light" and "dark" modes to your website can enhance your site's accessibility and make it feel more consistent with the host operating system's own theme.

With JavaScript (and React in particular) this is easy to do, as I'll explain in this post. We'll use a combination of our own JavaScript code, the Zustand state management library [1], and the styled components [2] package to achieve the following:

1

2

This post is written from the perspective of a GatsbyJS website, however the same concepts should apply to any React single-page application.

Setting up the theming

To start, add these new packages to the project:

yarn add zustand styled-components

Next, declare some themes! For this you can create a `themes.js` file (e.g. in your top level `src/` directory):

const themes = {
  light: {
    name: '☀️ Light Theme',
    background: '#FFFFFF',
    text: '#000000',
    headers: '#000000'
    links: '#01BAEF',
  },
  dark: {
    name: '🌒 Dark Theme',
    background: '#0F0E17',
    text: '#A7A9BE',
    headers: '#FFFFFE',
    links: '#FF8906',
  },
};
export default themes;

You can add as many attributes to your themes as you like in order to increase their flexibility. I use Happy Hues [3] for inspiration. You can also create as many different themes as you like!

3

Now we need a way to manage the themes and to maintain the currently-selected theme. This is where the Zustand library will come in useful for managing the app's global theme state.

For this, create a `store.js` file (e.g. again, this could be in your site's top-level `src/` directory) with these contents:

import create from 'zustand';

export default create(set => ({
  theme: 'light',
  setTheme: theme => set({ theme }),
}));

This tells Zustand to create a new store with two attributes: a reference to the current theme key (defaulting to `light`) and a function for changing the theme.

Next, use the `styled-components` library to create some dynamic components for use within the app that change their style based on the currently selected theme.

The package's `createGlobalStyle` function is great for injecting styles that affect the entire app. Import the function at the top of your primary layout file:

import { createGlobalStyle } from 'styled-components';

And then use it to create a global style component for your website based on the currently-selected theme (in the same top-level layout file):

const GlobalStyle = createGlobalStyle`
  body{
    background-color: ${({ theme }) => theme.background};
    color: ${({ theme }) => theme.text};
    transition: background-color 0.25s, color 0.25s;
  }
  h1,h2,h3,h4 {
    color: ${({ theme }) => theme.headers};
  }
  a {
    color: ${({ theme }) => theme.links};
  }
`;

We also added a transition to fade nicely between styles when the user later switches theme.

Finally, add some code (again, to the same layout file) to import the current theme from the store and pass this to the global style component we created.

// First import the store you created earlier near the top of the file:
import useStore from '../../store';

// Import the themes from the theme file you created earlier:
import themes from '../../themes';

...

// In your main layout component load the theme from the store:
const LayoutComponent = ({ children }) => {
  const theme = useStore(s => s.theme);

  ...
  
  // Then render the global styles and the layout's children:
  return (
    <div>
      <GlobalStyle theme={themes[theme]} />
      <div>{children}</div>
    </div>
  );
}

Your website is now using the theme to set the colours you defined in your theme object. If you change the default theme (in the `store.js` file) to `dark` and refresh the page, you should see the styles for the dark theme rendered.

Changing the theme

We now need to provide a way for the user to change the website's theme. Typically this might be done in a dedicated component elsewhere in your app, which is where using the Zustand store comes in handy.

Currently, on this website, I have a change theme select box in the site's `Header` component (as shown in the image below):

This can be achieved through something like the following:

// Import the styled-components library:
import styled from 'styled-components';

// Import the store we created earlier:
import useStore from '../store';

// Import the theme list we created earlier:
import themes from '../themes';

// Use styled-components to create a nice select box:
const StyledThemeSelector = styled.select`
  padding: 4px;
  background: white;
  border: 1px solid gray;
  border-radius: 5px;
  cursor: pointer;
`;

// In the component load the store:
const Header = () => {
  const store = useStore();

  // When rendering the component, include your nicely styled theme selector:
  return ( <>
    ...
    <StyledThemeSelector value={store.theme} onChange={e => store.setTheme(e.target.value)}>
      {Object.keys(themes).map(themeName =>
        <option key={themeName} value={themeName}>{themes[themeName].name}</option>
      )}
    </StyledThemeSelector>
    ...
  </> );
}

Now, when you switch theme using the select box, the entire app should change to reflect the new styles. Also, because you added the `transition` to the styles earlier, the new colours should fade in nicely rather than suddenly changing.

Of course, the theme-selctor can be any type of component - a button, a list of radio buttons, or anything else. If you have lots of themes you could even create a more visual theme gallery.

Remembering the selected theme

You can configure your website to remember the user's theme choice by leveraging `localStorage`.

To do so, first update your theme-selection component (as above) to implement a function which updates the site's local storage each time the user changes the theme:

...
const Header = () => {
  const store = useStore();

  // Add this function:
  const switchTheme = themeName => {
    store.setTheme(themeName);
    localStorage.setItem('theme', themeName);
  };

  // And update the onChange for your theme selector:
  return ( <>
    ...
    <StyledThemeSelector value={store.theme} onChange={e => switchTheme(e.target.value)}>
      ...
    </StyledThemeSelector>
    ...
  </> );
}

Then, back in your top-level layout file, add a `useEffect` hook to update the theme on app-load:

// Add the useEffect import:
import React, { useEffect } from 'react';
...

const LayoutComponent = ({ children }) => {
  const setTheme = useStore(s => s.setTheme);
  ...

  useEffect(() => {
    // Load and set the stored theme
    const rememberedTheme = localStorage.getItem('theme');
    if (rememberedTheme && themes[rememberedTheme]) {
      setTheme(rememberedTheme);
    } 
  }, [setTheme]);
   ...
}

Now the website will load the user's preferred theme each time they come back to your site.

Automatically matching the operating system theme

Many operating systems allow the user to choose between a light and dark theme, or even to dynamically change the theme based on the time of day.

It can be seen as good practice for websites to display a theme which more closely matches the background OS style to provide a more consistent user experience.

Luckily, this is very easy to do. Just update the `useEffect` block described above to check for the OS 'dark mode' as the app loads.

...
  useEffect(() => {
    const rememberedTheme = localStorage.getItem('theme');
    if (rememberedTheme && themes[rememberedTheme]) {
      setTheme(rememberedTheme);
    } else {
      const isDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
      if (isDarkMode) setTheme('dark');
    }
  }, [setTheme]);
...

The nice thing about this approach is that if the user has already chosen another theme preference, this will be used instead of the default.

Complete code

I hope this post helps you to create your own theme libraries and nice style-switchers. As mentioned earlier, you could create as many themes as you like and make them as complex as necessary.

You can also import the store into any component if you want more fine-grained control (e.g. themed message boxes), and even include fonts and other types of styles.

For the complete code discussed in this post, see below.

const themes = {
  light: {
    name: '☀️ Light Theme',
    background: '#FFFFFF',
    text: '#000000',
    headers: '#000000'
    links: '#01BAEF',
  },
  dark: {
    name: '🌒 Dark Theme',
    background: '#0F0E17',
    text: '#A7A9BE',
    headers: '#FFFFFE',
    links: '#FF8906',
  },
};
export default themes;
import create from 'zustand';

export default create(set => ({
  theme: 'light',
  setTheme: theme => set({ theme }),
}));
import React, { useEffect } from 'react';
import { createGlobalStyle } from 'styled-components';
import useStore from '../../store';
import themes from '../../themes';

const GlobalStyle = createGlobalStyle`
  body{
    background-color: ${({ theme }) => theme.background};
    color: ${({ theme }) => theme.text};
    transition: background-color 0.25s, color 0.25s;
  }
  h1,h2,h3,h4 {
    color: ${({ theme }) => theme.headers};
  }
  a {
    color: ${({ theme }) => theme.links};
  }
`;

const LayoutComponent = ({ children }) => {
  const theme = useStore(s => s.theme);
  const setTheme = useStore(s => s.setTheme);
  ...
  useEffect(() => {
    const rememberedTheme = localStorage.getItem('theme');
    if (rememberedTheme && themes[rememberedTheme]) {
      setTheme(rememberedTheme);
    } else {
      const isDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
      if (isDarkMode) setTheme('dark');
    }
  }, [setTheme]);
  ...
  return (
    <div>
      <GlobalStyle theme={themes[theme]} />
      <div>{children}</div>
    </div>
  );
}
export default LayoutComponent;
import React from 'react';
import styled from 'styled-components';
import useStore from '../store';
import themes from '../themes';

const StyledThemeSelector = styled.select`
  padding: 4px;
  background: white;
  border: 1px solid gray;
  border-radius: 5px;
  cursor: pointer;
`;

const Header = () => {
  const store = useStore();

  const switchTheme = themeName => {
    store.setTheme(themeName);
    localStorage.setItem('theme', themeName);
  };

  return ( <>
    ...
    <StyledThemeSelector value={store.theme} onChange={e => switchTheme(e.target.value)}>
      {Object.keys(themes).map(themeName =>
        <option key={themeName} value={themeName}>{themes[themeName].name}</option>
      )}
    </StyledThemeSelector>
    ...
  </> );
}
export default Header;

Reply via email

Back to gemlog