Let's figure out how to make bulletproof dark and light mode support with styled components for a React app.
import { ComponentWithChildrenProps } from "lib/shared/props"
import { PrefferedThemeProvider } from "lib/ui/theme/PrefferedThemeProvider"
import { ThemePreference } from "lib/ui/theme/ThemePreference"
import {
PersistentStorageKey,
usePersistentStorageValue,
} from "state/persistentStorage"
export const ThemeProvider = ({ children }: ComponentWithChildrenProps) => {
const [prefferedTheme, setPrefferedTheme] =
usePersistentStorageValue<ThemePreference>(
PersistentStorageKey.ThemePreference,
"system"
)
return (
<PrefferedThemeProvider
prefferedTheme={prefferedTheme}
setPrefferedTheme={setPrefferedTheme}
>
{children}
</PrefferedThemeProvider>
)
}
Here we have a theme provider that takes the current theme name from local storage and passes it to the PrefferedThemeProvider
component with a function for changing the theme. You can find a video on local storage and React here. In the PrefferedThemeProvider
component, we check if the system color is dark with a media query and update the current theme in the use effect hook. To make styled components aware of the selected theme, we use ThemeProvider. Also, we want to provide a way for the user to change between dark and light modes, so we have a provider for PrefferedThemeContext. In the demo, we have a ThemeToggleButton, that will change the theme to the opposite one. At increaser.org, I have a theme selection on the settings page, where you can also select a system mode.
import { createContext, useEffect, useMemo, useState } from "react"
import { useMedia } from "react-use"
import { ComponentWithChildrenProps } from "lib/shared/props"
import { DefaultTheme, ThemeProvider } from "styled-components"
import { darkTheme } from "lib/ui/theme/darkTheme"
import { lightTheme } from "./lightTheme"
import { ThemePreference } from "./ThemePreference"
import { createContextHook } from "lib/shared/utils/createContextHook"
interface PrefferedThemeState {
prefferedTheme: ThemePreference
setPrefferedTheme: (theme: ThemePreference) => void
}
const PrefferedThemeContext = createContext<PrefferedThemeState | undefined>(
undefined
)
type Props = PrefferedThemeState & ComponentWithChildrenProps
export const PrefferedThemeProvider = ({
prefferedTheme,
setPrefferedTheme,
children,
}: Props) => {
const isSystemThemeDark = useMedia("(prefers-color-scheme: dark)", false)
const [theme, setTheme] = useState<DefaultTheme>(darkTheme)
useEffect(() => {
if (prefferedTheme === "system") {
setTheme(isSystemThemeDark ? darkTheme : lightTheme)
} else {
setTheme(prefferedTheme === "dark" ? darkTheme : lightTheme)
}
}, [isSystemThemeDark, prefferedTheme])
return (
<PrefferedThemeContext.Provider
value={{ prefferedTheme, setPrefferedTheme }}
>
<ThemeProvider theme={theme}>{children}</ThemeProvider>
</PrefferedThemeContext.Provider>
)
}
export const usePrefferedTheme = createContextHook(
PrefferedThemeContext,
"PrefferedThemeContext"
)
I took theme inspiration from Notion, which has quite a minimalistic setup. Both themes have a few shared colors: primary, attention, alert, success, and white. While background, foreground, glass, and text colors are unique to each mode. For most cases, components consume colors without checking for theme name, except in rare situations like a Card component that doesn't change a background in the light mode.