Hi 👋
Thank you for taking time to read this article!
One of the most common features we implement is dark mode. However, after implementing dark mode once in a project, it's easy to forget how it was implemented because there's no opportunity to implement it for a while.
So I decided to put together an article on the method I use most often so that everyone can check it anytime!
Table of Contents
1. Tech stack
2. Step-by-step implementation
2.1. Configs, constants and types for color themes
2.2. Set up Redux Toolkit
2.3. Utility functions to handle color theme cookie
2.4. Implement useColorTheme custom hook
2.5. Configure RTK and Styled Components in _app.tsx
3. Let's implement dark mode toggle switch
Tech stack
The language, libraries and frameworks used for the implementation are as follows:
- Next.js
- TypeScript
- Styled Components
- Redux Toolkit
- cookies-next
Step-by-step implementation
Configs, constants and types for color themes
First, let's define the constants and types needed to manage the color theme.
in const/colorTheme.ts
// const/colorTheme.ts
// Types of available color themes
export const colorThemeNames = [
'light',
'dark',
] as const;
// Can't use type ColorThemeName because of circular dependency
export const defaultColorThemeName: typeof colorThemeNames[number] = 'light';
// Cookie key for color theme
export const colorThemeCookieName = 'myAppColorTheme';
and
in types/colorTheme.ts
// types/colorTheme.ts
import { colorThemeNames } from '../const/colorTheme';
export type ColorThemeStyle = {
colors: {
text: string
background: string
componentBackground: string
border: string
info: string
infoBg: string
danger: string
dangerBg: string
},
};
export type ColorThemeName = typeof colorThemeNames[number];
/**
* Type guard for ColorThemeName
*
* @param {unknown} val
* @return {*} {val is ColorThemeName}
*/
export const isColorThemeName = (val: unknown): val is ColorThemeName => (
colorThemeNames.includes(val as ColorThemeName)
);
Also we need to modify DefaultTheme
type in styled.d.ts
like this.
// styled.d.ts
import 'styled-components';
import { ColorThemeStyle } from './types/colorTheme';
declare module 'styled-components' {
export interface DefaultTheme extends ColorThemeStyle {}
}
It's convenient to create variables for the colors we will use.
// const/styles/colors.tsx
export const dryadBark = '#37352f'; // light theme string color
export const white = '#ffffff'; // light theme component color
export const errigalWhite = '#f6f6f9'; // light theme background color
export const gainsboro = '#d9d9d9'; // light theme border color
export const coralRed = '#f93e3d'; // common danger color
export const translucentUnicorn = '#fcecee';
export const softPetals = '#e9f6ef';
export const vegetation = '#48cd90'; // common info color
export const stonewallGrey = '#c3c2c1';
export const astrograniteDebris = '#3b414a'; // dark theme border color
export const aswadBlack = '#141519'; // dark theme background color
export const washedBlack = '#202528'; // dark theme component background color
(I used this library to name these variables.)
Then declare color theme objects, default theme.
config/styles/colorTheme.ts
import { ColorThemeStyle, ColorThemeName } from '../../types/colorTheme';
// colors
import {
dryadBark,
white,
errigalWhite,
gainsboro,
coralRed,
vegetation,
astrograniteDebris,
aswadBlack,
washedBlack,
softPetals,
translucentUnicorn,
} from '../../const/styles/colors';
export const defaultColorThemeName: ColorThemeName = 'light';
export const lightTheme: ColorThemeStyle = {
colors: {
text: dryadBark,
background: errigalWhite,
componentBackground: white,
border: gainsboro,
info: vegetation,
infoBg: softPetals,
danger: coralRed,
dangerBg: translucentUnicorn,
},
};
export const darkTheme: ColorThemeStyle = {
colors: {
text: white,
background: aswadBlack,
componentBackground: washedBlack,
border: astrograniteDebris,
info: vegetation,
infoBg: softPetals,
danger: coralRed,
dangerBg: translucentUnicorn,
},
};
export const themeNameStyleMap: { [key in ColorThemeName]: ColorThemeStyle } = {
light: lightTheme,
dark: darkTheme,
};
export const defaultColorThemeStyle = themeNameStyleMap[defaultColorThemeName];
Set up Redux Toolkit
The state for dark mode must be managed globally. So let's use Redux Toolkit to manage global state.
We are going to create colorThemeSlice
, typed useDispatch
, typed useSelector
and configure store
.
stores/slices/colorThemeSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
// "type" is needed. If no "type", circular dependency error arise
// https://stackoverflow.com/questions/63923025/how-to-fix-circular-dependencies-of-slices-with-the-rootstate
import type { RootState } from '../store';
import { ColorThemeName } from '../../types/colorTheme';
import { defaultColorThemeName } from '../../const/colorTheme';
type ColorThemeState = {
theme: ColorThemeName
};
const initialState: ColorThemeState = {
theme: defaultColorThemeName,
};
const colorThemeSlice = createSlice({
name: 'colorTheme',
initialState,
reducers: {
updateColorTheme: (state, action: PayloadAction<ColorThemeName>) => {
state.theme = action.payload;
},
},
});
// selectors
export const selectColorTheme = (state: RootState) => state.colorTheme.theme;
export default colorThemeSlice.reducer;
// actions
export const {
updateColorTheme,
} = colorThemeSlice.actions;
stores/store.ts
// https://redux-toolkit.js.org/tutorials/typescript
import { configureStore } from '@reduxjs/toolkit';
// reducers
import colorThemeReducer from './slices/colorThemeSlice';
export const store = configureStore({
reducer: {
colorTheme: colorThemeReducer,
},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
stores/hooks.ts
// https://redux-toolkit.js.org/tutorials/typescript
import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux';
import { RootState, AppDispatch } from './store';
export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
Utility functions to handle color theme cookie
Dark mode must persists even when the page is refreshed. To achieve this, we are going to use cookie.
utils/cookie/colorTheme.ts
import { getCookie, setCookie } from 'cookies-next';
import { OptionsType } from 'cookies-next/lib/types';
import { colorThemeCookieName } from '../../const/colorTheme';
import { ColorThemeName, isColorThemeName } from '../../types/colorTheme';
/**
* Set color theme cookie to persist color theme config
*
* @param {ColorThemeName} value
* @param {OptionsType} [options]
*/
export const setColorThemeCookie = (value: ColorThemeName, options?: OptionsType) => {
setCookie(colorThemeCookieName, value, options);
};
/**
*
*
* @param {OptionsType} [options]
* @return {string} {string}
*/
export const getColorThemeCookie = (options?: OptionsType): string => {
const colorThemeCookie = getCookie(colorThemeCookieName, options);
return isColorThemeName(colorThemeCookie) ? colorThemeCookie : '';
};
Implement useColorTheme custom hook
In the implementation, we'll need to switch color themes or retrieve the current color theme. Let's put those logics into a custom hook so that they can be called in every components.
hooks/useColorTheme.ts
import { defaultColorThemeName } from '../const/colorTheme';
import { getColorThemeCookie, setColorThemeCookie } from '../utils/cookie/colorTheme';
import { useAppDispatch, useAppSelector } from '../stores/hooks';
import { selectColorTheme, updateColorTheme } from '../stores/slices/colorThemeSlice';
import { themeNameStyleMap } from '../config/styles/colorThemes';
import { ColorThemeName, ColorThemeStyle, isColorThemeName } from '../types/colorTheme';
/**
* Custom hook for handling color themes
*
*/
const useColorTheme = () => {
const dispatch = useAppDispatch();
const currentColorTheme = useAppSelector(selectColorTheme);
/**
* Set color theme cookie and state
*
* @param {ColorThemeName} colorThemeName
*/
const setColorTheme = (colorThemeName: ColorThemeName) => {
setColorThemeCookie(colorThemeName);
dispatch(updateColorTheme(colorThemeName));
};
/**
* Initialize color theme cookie and state
*
* @return {void}
*/
const initColorTheme = () => {
const currentColorThemeCookie = getColorThemeCookie();
if (!currentColorThemeCookie || !isColorThemeName(currentColorThemeCookie)) {
setColorTheme(defaultColorThemeName);
return;
}
dispatch(updateColorTheme(currentColorThemeCookie));
};
/**
*
*
* @return {*} ColorTheme
*/
const getCurrentColorThemeState = (): ColorTheme => (
currentColorThemeState
);
/**
*
*
* @return {*} {ColorThemeStyle}
*/
const getCurrentColorThemeStyle = (): ColorThemeStyle => (
themeNameStyleMap[currentColorTheme]
);
return {
setColorTheme,
initColorTheme,
getCurrentColorThemeState,
getCurrentColorThemeStyle,
};
};
export default useColorTheme;
Configure RTK and Styled Components in _app.tsx
We've implemented so many functions and logic up to this point, but as it stands, we can't use them yet.
Let's edit _app.tsx to make sure Redux Toolkit and Styled Components are available for use.
pages/_app.tsx
import { useEffect, ReactElement, ReactNode } from 'react';
// Next
import { NextPage } from 'next';
import { Router } from 'next/router';
import type { AppProps } from 'next/app';
// Libraries
import { Provider } from 'react-redux';
import { ThemeProvider } from 'styled-components';
import { store } from '../stores/store';
import GlobalStyle from '../components/globalstyles';
import useColorTheme from '../hooks/useColorTheme';
// Layout configuration doc
// https://nextjs.org/docs/pages/building-your-application/routing/pages-and-layouts#with-typescript
export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
getLayout?: (page: ReactElement) => ReactNode;
};
type AppPropsWithLayout = AppProps & {
Component: NextPageWithLayout
router: Router // Error if this property doesn't exist
};
/**
*
*
* @param {AppPropsWithLayout} { Component, pageProps }
* @return {*} JSX.Element
*/
const WithThemeProviderComponent = ({ Component, pageProps }: AppPropsWithLayout) => {
const { initColorTheme, getCurrentColorThemeStyle } = useColorTheme();
useEffect(() => {
initColorTheme();
}, []);
return (
<ThemeProvider theme={getCurrentColorThemeStyle()}>
<GlobalStyle />
<Component {...pageProps} />
</ThemeProvider>
);
};
const App = ({ Component, pageProps, router }: AppPropsWithLayout) => {
const getLayout = Component.getLayout ?? ((page) => page);
return (
<Provider store={store}>
{getLayout(
<WithThemeProviderComponent
Component={Component}
pageProps={pageProps}
router={router}
/>,
)}
</Provider>
);
};
export default App;
Let's implement dark mode toggle switch
With the implementation up to this point, we've completed the necessary preparations for switching the color theme.
Now, let's use useColorTheme
to implement DarkModeToggleSwitch
component (we'll skip detailed styling for now).
globalstyles.tsx
...
body {
...
background-color: ${({ theme }) => theme.colors.background};
color: ${({ theme }) => theme.colors.text};
...
}
...
components/DarkModeToggleSwitch.tsx
import { useColorTheme } from '../../../hooks/useColorTheme'
/**
* Dark mode <-> light mode toggle switch
* Update cookie value and global state
*
* @return {*} JSX.Element
*/
const DarkModeToggleSwitch = () => {
const { setColorTheme, getCurrentColorThemeState } = useColorTheme()
const currentColorTheme = getCurrentColorThemeState()
const isDark = currentColorTheme === 'dark'
const toggleDarkTheme = () => {
isDark ? setColorTheme('light') : setColorTheme('dark')
}
return (
<>
...
<input type='checkbox' checked={isDark} onChange={toggleDarkTheme} />
...
</>
)
}
export default DarkModeToggleSwitch
That's all.
How was it? I hope this article was helpful for you.
There are many ways to implement color theme switching besides this article. If you have any recommended methods or articles, please share them in the comments and let me know!
And if you found this article helpful, please share it on social media!
Top comments (1)
Hello koyablue
ColorTheme
andcurrentColorThemeState
are not known in the code snippet below and the errorCannot find name 'ColorTheme'.ts(2304)
andCannot find name 'currentColorThemeState'. Did you mean 'getCurrentColorThemeState'? ts(2552)
is given.const getCurrentColorThemeState = (): ColorTheme => (
currentColorThemeState
);