Lewislbr

Add dark mode to a React with TypeScript and styled-components app

In this post we'll see how to add dark mode to an application made with React, TypeScript, and styled-components, and store the choice into local storage so it's saved for following visits.

Define Global Styles

Firstly, we'll define some global styles using styled-components's createGlobalStyle function, where we'll use CSS variables to define the base colors, and declare each element color conditionally depending on the theme prop we'll pass later on:

import {createGlobalStyle, css} from "styled-components"

interface Props {
  theme: string
}

export const GlobalStyles = createGlobalStyle(
  (props: Props) => css`
    :root {
      --color-dark: hsl(0, 0%, 10%);
      --color-light: hsl(0, 0%, 95%);
    }

    body {
      background-color: ${props.theme === "light" ? "var(--color-light)" : "var(--color-dark)"};
      color: ${props.theme === "light" ? "var(--color-dark)" : "var(--color-light)"};
    }
  `,
)

Create A Theme Context

Then, using a modified version of useHooks's useLocalStorage Hook to store the chosen theme in the browser's local storage, we'll create a global state with React's Context API, to pass the theme variable an the updater function to the Provider and GlobalStyles components that will wrap the whole application:

import React, {createContext, useState} from "react"
import {GlobalStyles} from "../components"

function useLocalStorage(key: string, initialValue: string): [string, Function] {
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key)
      return item ? JSON.parse(item) : initialValue
    } catch (error) {
      console.log(error)
      return initialValue
    }
  })
  const setValue = (valueToStore: string): void => {
    try {
      setStoredValue(valueToStore)
      window.localStorage.setItem(key, JSON.stringify(valueToStore))
    } catch (error) {
      console.log(error)
    }
  }

  return [storedValue, setValue]
}

interface ContextProps {
  theme: string
  setTheme: Function
}

export const ThemeContext = createContext<ContextProps>({
  theme: "",
  setTheme: () => null,
})

interface Props {
  children: React.ReactNode
}

export function ThemeProvider(props: Props): JSX.Element {
  const [theme, setTheme] = useLocalStorage("theme", "light")

  return (
    <ThemeContext.Provider value={{theme, setTheme}}>
      <GlobalStyles theme={theme} />
      {props.children}
    </ThemeContext.Provider>
  )
}

Add A Theme Toggle Button

Finally, we'll create a button than can change the theme variable and the elements styles will update accordingly:

import React, { useContext } from 'react';
import { ThemeContext } from '../contexts';

export function ThemeButton(): JSX.Element {
  const { theme, setTheme } = useContext(ThemeContext);

  function swapTheme {
    if (theme === 'light') {
      setTheme('dark');
    } else {
      setTheme('light');
    }
  };

  return (
    <button onClick={swapTheme}>
      {theme === 'light' ? 'Change to dark mode' : 'Change to light mode'}
    </button>
  );
}



If you're using dark mode, do you like the code blocks's theme? I have it available for VS Code, feel free to check it.