How to create light and dark modes

How to

Software development

Written by

Daniel Sadler

Date

2 years ago

Read time

6 minutes

So you've got an idea for a website or app, and as you start building, you realise you want to add a light and dark mode theme to enhance the user experience. It might seem like a minor feature, but it can greatly impact the overall look and feel of your project. In this blog post, we'll guide you through creating a seamless and effective light and dark theme for your website or app, taking you through the key considerations and best practices every step of the way.

First, we must ask the question, why do we need a dark mode?

The need for a dark mode in websites and applications has become increasingly popular in recent years. This is due to the growing demand for customisation and personalisation in user experiences. Some users find dark mode easier on the eyes and more stylish, while others appreciate the energy-saving benefits on OLED displays. Others still prefer the traditional look of light mode, finding it easier to read. Regardless of personal preference, it's a good practice to provide users with the choice between a light and dark theme.

Prerequisites

To complete this tutorial, you will need to have a basic understanding of the following:

    React

    Typescript

    Tailwind CSS (optional, but preferred for styling)

    Headless UI (optional, but recommended for ease of use)

    Next.JS (optional, should work similarly for any framework)

Notes: If you prefer not to use Tailwind CSS, you can switch the id/class of your component based on the 'isDarkMode' boolean that we will define later. Similarly, while Headless UI is convenient, any button can be used in place of its toggle component.

If you haven’t already created your Next.JS app then follow this simple tutorial here, made by the team at Vercel.

https://nextjs.org/learn/basics/create-nextjs-app

How it’s done

We are going to be delving into React Contexts, if you are unfamiliar with Context then the react docs do a great job of explaining. But a basic summary of Context is it’s a way to share data between components without using a traditional "top-down" approach of passing props down through multiple levels of components.

To start, create a new file named DarkMode.tsx within your components folder.

We will first create a type called DarkModeType, where we'll define isDarkMode as a boolean value and toggleDarkMode as a function. Then, we'll create a context called DarkModeContext that uses this type.

type DarkModeType = { isDarkMode: boolean; toggleDarkMode: () => void }; const DarkModeContext = createContext<DarkModeType>({} as DarkModeType);

Next, we create a provider component that takes in one prop: children (which is just the content that this provider will wrap).

We create our stateful variable isDarkMode and set it to false.

We also define a toggleDarkMode function, which is called whenever the user wants to switch between dark and light modes. This function updates the isDarkMode value and also stores it in localStorage.

Then, we need a useEffect which will handle preserving the previous state when a user does anything that might cause a refresh of the DOM and also will default to the user's OS settings if they haven’t explicitly set the theme. We will be checking to see if there is a value defined in local storage and if there is, default to that value.

Lastly, we will return our <DarkModeContext.Provider>. Passing in two values, our isDarkMode variable and our toggleDarkMode function.

Inside this, wrap our children prop in a div. This div will contain an optional className dark which will be applied if isDarkMode is true.

interface Props { children?: React.ReactNode; } export const DarkMode = ({ children }: Props) => { const [isDarkMode, setDarkMode] = useState(false); const toggleDarkMode = useCallback(() => { localStorage.setItem('theme', !isDarkMode ? 'dark' : 'light'); setDarkMode(!isDarkMode); }, [setDarkMode, isDarkMode]); useEffect(() => { const storedTheme = localStorage.getItem('theme'); setDarkMode( storedTheme ? storedTheme === 'dark' : window.matchMedia('(prefers-color-scheme: dark)').matches ); }, []); To make accessing these context values easier, we have also created a custom hook called `useDarkMode`. This hook allows us to retrieve the context values provided by the `DarkMode` component so that we can easily use and toggle the dark mode in other parts of our application. ```tsx export const useDarkMode = () => useContext(DarkModeContext);

Pulling all this together should result in a file that looks like this.

//DarkMode.tsx import React, { createContext, useCallback, useContext, useEffect } from 'react'; type DarkModeType = { isDarkMode: boolean; toggleDarkMode: () => void }; const DarkModeContext = createContext<DarkModeType>({} as DarkModeType); interface Props { children?: React.ReactNode } export const DarkMode = ({ children }: Props) => { const [isDarkMode, setDarkMode] = useState(false); const toggleDarkMode = useCallback(() => { localStorage.setItem('theme', !isDarkMode ? 'dark' : 'light'); setDarkMode(!isDarkMode); }, [setDarkMode, isDarkMode]); useEffect(() => const storedTheme = localStorage.getItem('theme'); setDarkMode( storedTheme ? storedTheme === 'dark' : window.matchMedia('(prefers-color-scheme: dark)').matches ); }, [setDarkMode]); return ( <DarkModeContext.Provider value={{ isDarkMode, toggleDarkMode }}> <div className={isDarkMode ? `dark` : ''}>{children}</div> </DarkModeContext.Provider> ); }; export const useDarkMode = () => useContext(DarkModeContext);

As we are using Next.JS we are going to be adding our <DarkMode /> component into our root file, _app.tsx. We need to wrap our <Component /> in our <DarkMode /> component. This allows us to access the DarkModeContext at any level within our component tree.

//_app.tsx const App = ({ Component, pageProps }: AppProps) => { return ( <div> <Head> <title>Light and Dark</title> <meta name="description" content="Light and dark mode tutorial" /> <link rel="icon" href="/favicon.ico" /> </Head> <DarkMode> <Component {...pageProps} /> </DarkMode> </div> ); }; export default App;

If you are not using tailwind for your project then please skip this section.

To take advantage of the dark variant we added earlier, we need to modify our Tailwind config file. By setting darkMode: class, we can use the dark: Tailwind variant to apply styles to any element within a parent element with the class dark. Any styles defined after the dark keyword will only be applied when dark is specified in the top-level div. This feature, by default, is set to media mode, which changes the theme based on the user's device. However, since we want to give the user the option to switch between light and dark modes, we need to change it to 'class' mode.

//tailwind.config.ts /** @type {import('tailwindcss').Config} */ module.exports = { content: [ './pages/**/*.{js,ts,jsx,tsx}', './src/components/**/*.{js,ts,jsx,tsx}', ], theme: { extend: {}, }, plugins: [], darkMode: 'class', };

To put all our work into action, we need to import the useDarkMode context which includes the isDarkMode boolean and the toggleDarkMode function. We already have the dark attribute defined, so why do we need the isDarkMode boolean? Anything we may want to change based on this state that isn’t styling would not see the ‘dark’ attribute so we can use this boolean instead. Additionally, the toggle component requires a boolean, and we can use our global state instead of creating another.

To style your page, feel free to choose any design you like. Just keep in mind that for any background or text colours you add, you should also include a dark alternative.

//Home.tsx export const Home = () => { const { isDarkMode, toggleDarkMode } = useDarkMode(); return ( <div className="flex flex-col space-y-2 justify-center bg-gray-50 dark:bg-gray-900 h-screen w-full items-center "> <section className="border-2 border-yellow-500 flex flex-col w-1/2 py-4"> <header className="flex justify-center"> <h1 className="font-semibold text-7xl text-gray-900 dark:text-gray-300"> {isDarkMode ? 'Dark' : 'Light'} </h1> </header> <div className="flex justify-center"> <div className="flex flex-row items-center space-x-2"> <SunIcon height={24} width={24} className="text-gray-900 dark:text-yellow-500" /> <Toggle checked={isDarkMode} onChange={toggleDarkMode} /> <MoonIcon height={24} width={24} className="text-gray-900 dark:text-yellow-500" /> </div> </div> </section> <a href="/test" className="border-2 rounded-full border-yellow-500 bg-transparent text-gray-900 dark:text-gray-300 p-2" > Go to test page! </a> </div> ); };
"text-DarkBlueGray dark:text-BrightYellow"

A basic example of how this might work without tailwind would be to use the isDarkMode boolean to switch the component's class.

Our top level <div> would instead look like this:

<div className={`wrapper ${isDarkMode ? "wrapper-dark" : "wrapper-light"}`}>

And our CSS file would look like this:

.wrapper{ display: flex; margin-top: 0.5rem; flex-direction: column; justify-content: center; align-items: center; width: 100%; height: 100vh; } .wrapper-dark{ background: #1C1C1E } .wrapper-light{ background: #F9F9FB }

I have a Toggle component, using HeadlessUI, however, any call-to-action will work. The style of the site is up to you! I have provided an example below of the component I am using.

I am passing in the props of Switch to keep the prop types more consistent as Toggle is an extension of the Switch component. And then spreading the prop types down.

//Toggle.tsx import { Switch } from '@headlessui/react'; import { ComponentType } from 'react'; type ExtractProps<T> = T extends ComponentType<infer P> ? P : T; export const Toggle = (props: ExtractProps<typeof Switch>) => { return ( <div className="py-10"> <Switch className="bg-gray-900 dark:bg-yellow-500 ..." {...props} > <span aria-hidden="true" className={`${props.checked ? 'translate-x-9' : 'translate-x-0'} ...`} /> </Switch> </div> ); };

Congratulations, you have successfully created a dark mode context for your website! With this in place, users can easily switch between light and dark modes to better suit their preferences and needs.

However, it's important to keep accessibility concerns in mind when implementing dark mode. For instance, you'll want to ensure that there is enough contrast between the text and background colours, particularly in dark mode, to make sure that everyone can easily read your content.

2023-02-16 17.02.56

person with an email icon

Subscribe to our newsletter

Be the first to know about our latest updates, industry trends, and expert insights

Your may unsubscribe from these communications at any time. For information please review our privacy policy.