Nowadays, dark mode on web pages becomes popular because it provides more comfort for some people, including me. Using Tailwind we can configure it easily and choose one of 2 available strategies, the media strategy which leverage preferes-color-scheme
CSS media feature and class
strategy which use dark
class in HTML document. In this article, we will enable Tailwind dark mode in the Next.js project, and also configure Storybook to simulate dark mode in component documentation.
Configure the dark mode
Starting from this step, I assume we already have a project bootstrapped using Next.js and Tailwind. If you need help setting it up, hopefully, this article can be useful. Within this step, we would like to configure the dark mode using class
strategy.
- Configure dark mode
class
strategy intailwind.config.js
1module.exports = { 2 ... 3 darkMode: 'class', 4 ... 5};
- Update
styles/global.css
to switch background and text color based color mode1@tailwind base; 2@tailwind components; 3@tailwind utilities; 4 5body { 6 @apply bg-white text-slate-800 dark:bg-slate-900 dark:text-slate-300; 7}
We use
dark:
prefix to apply specific style only for dark mode. - Also, add some dark mode styling in
pages/index.tsx
1import type { NextPage } from 'next'; 2import Head from 'next/head'; 3 4const Home: NextPage = () => { 5 return ( 6 <> 7 <Head> 8 <title>Next.js Tailwind</title> 9 <meta content="Generated by create next app" name="description" /> 10 <link href="/favicon.ico" rel="icon" /> 11 </Head> 12 13 <div className="bg-primary-500 absolute left-0 top-64 -z-10 h-72 w-72 rounded-full opacity-10 blur-3xl"></div> 14 <div className="bg-danger-500 absolute right-0 top-24 -z-10 h-72 w-72 rounded-full opacity-10 blur-3xl"></div> 15 16 <section className="container mx-auto py-16 px-6 text-center sm:py-40"> 17 <h1 className="mx-auto w-3/4 pb-10 text-4xl font-bold text-slate-900 dark:text-slate-100 sm:text-5xl"> 18 Next.js Tailwind 19 </h1> 20 <p>A great combination of Next.js and Tailwind.</p> 21 </section> 22 </> 23 ); 24}; 25 26export default Home;
In the above example, dark mode styling is added in
h1
tag. - Let’s test the configuration by running
yarn dev
, then adddark
class into the HTML.Light modeDark mode activated and style with dark: prefix is applied
Create a color mode switcher component
To make it easier for switching between color modes, let’s create a component for it.
- Create a component named
components/ColorModeToggle.tsx
1export interface ColorModeToggleProps { 2 className?: string; 3 dark?: boolean; 4 onChange?: (dark: boolean) => void; 5} 6 7export default function ColorModeToggle({ 8 className = '', 9 dark, 10 onChange, 11}: ColorModeToggleProps) { 12 return ( 13 <div 14 className={`relative flex h-8 w-20 cursor-pointer rounded-full bg-slate-50 dark:bg-slate-800 ${className}`} 15 onClick={() => onChange?.(!dark)} 16 > 17 <div 18 className={`absolute h-8 w-8 rounded-full shadow transition-transform ${ 19 dark 20 ? 'translate-x-12 bg-slate-800 shadow-slate-800 dark:bg-cyan-700 dark:shadow-cyan-700' 21 : 'left-0 bg-amber-300 shadow-amber-300' 22 }`} 23 ></div> 24 <div 25 className={`w-full px-3 text-sm leading-8 transition-all dark:text-slate-400 ${ 26 dark ? 'text-left' : 'text-right' 27 }`} 28 > 29 {dark ? 'Dark' : 'Light'} 30 </div> 31 </div> 32 ); 33}
- Create
providers/ColorModeProvider.tsx
and leverage React Context to save our color mode states as a global state so it can be easily reused across components1import { 2 createContext, 3 ReactNode, 4 useCallback, 5 useEffect, 6 useState, 7} from 'react'; 8import { useRouter } from 'next/router'; 9 10export interface ColorModeContextValue { 11 dark: boolean; 12 toggleColorMode: (becomeDark: boolean) => void; 13} 14 15export const ColorModeContext = createContext<ColorModeContextValue>({ 16 dark: false, 17 toggleColorMode: () => { 18 /*do nothing */ 19 }, 20}); 21 22export interface ColorModeProviderProps { 23 children?: ReactNode; 24} 25 26export default function ColorModeProvider({ 27 children, 28}: ColorModeProviderProps) { 29 const [dark, setDark] = useState(false); 30 const router = useRouter(); 31 32 const setDarkClassName = useCallback((becomeDark: boolean) => { 33 if (becomeDark) { 34 global.document.documentElement.classList.add('dark'); 35 } else { 36 global.document.documentElement.classList.remove('dark'); 37 } 38 }, []); 39 40 const toggleColorMode = (becomeDark: boolean) => { 41 setDark(becomeDark); 42 setDarkClassName(becomeDark); 43 global.localStorage.theme = becomeDark ? 'dark' : 'light'; 44 }; 45 46 useEffect(() => { 47 const isCurrentlyDark = 48 router.query?.theme === 'dark' || window.localStorage?.theme === 'dark'; 49 setDark(isCurrentlyDark); 50 setDarkClassName(isCurrentlyDark); 51 }, [router, setDark, setDarkClassName]); 52 53 return ( 54 <ColorModeContext.Provider 55 value={{ 56 dark, 57 toggleColorMode, 58 }} 59 > 60 {children} 61 </ColorModeContext.Provider> 62 ); 63}
- Put the provider in
pages/_app.tsx
1import '../styles/globals.css'; 2 3import type { AppProps } from 'next/app'; 4 5import ColorModeProvider from 'providers/ColorModeProvider'; 6 7function MyApp({ Component, pageProps }: AppProps) { 8 return ( 9 <ColorModeProvider> 10 <Component {...pageProps} /> 11 </ColorModeProvider> 12 ); 13} 14 15export default MyApp;
- If you find a TypeScript error in
pages/_app.tsx
saying “Cannot find module ‘providers/ColorModeProvider’” like this, then addbaseUrl
intotsconfig.json
TypeScript cannot find providers directory1{ 2 "compilerOptions": { 3 "baseUrl": ".", 4 ... 5 } 6}
Re-open
pages/_app.tsx
to see the effect. - Create
components/Header.tsx
to place the toggle component1import { useContext } from 'react'; 2 3import { ColorModeContext } from 'providers/ColorModeProvider'; 4 5import ColorModeToggle from './ColorModeToggle'; 6 7export default function Header() { 8 const { dark, toggleColorMode } = useContext(ColorModeContext); 9 10 return ( 11 <header className="container mx-auto flex items-center justify-end p-6"> 12 <ColorModeToggle 13 className="ml-4" 14 dark={dark} 15 onChange={toggleColorMode} 16 /> 17 </header> 18 ); 19}
- Reuse header component in any pages, for example in
pages/index.tsx
1import type { NextPage } from 'next'; 2import Head from 'next/head'; 3import Header from 'components/Header'; 4 5const Home: NextPage = () => { 6 return ( 7 <> 8 <Head> 9 <title>Next.js Tailwind</title> 10 <meta content="Generated by create next app" name="description" /> 11 <link href="/favicon.ico" rel="icon" /> 12 </Head> 13 14 <Header /> 15 16 <div className="bg-primary-500 absolute left-0 top-64 -z-10 h-72 w-72 rounded-full opacity-10 blur-3xl"></div> 17 <div className="bg-danger-500 absolute right-0 top-24 -z-10 h-72 w-72 rounded-full opacity-10 blur-3xl"></div> 18 19 <section className="container mx-auto py-16 px-6 text-center sm:py-40"> 20 <h1 className="mx-auto w-3/4 pb-10 text-4xl font-bold text-slate-900 dark:text-slate-100 sm:text-5xl"> 21 Next.js Tailwind 22 </h1> 23 <p>A great combination of Next.js and Tailwind.</p> 24 </section> 25 </> 26 ); 27}; 28 29export default Home;
- Let’s test the configuration by switching the color mode using our new component.
Configure dark mode in Storybook
There are several alternatives to enable dark mode in Storybook but I use storybook-dark-mode addon because my plan is to make the whole parts in Storybook UI changed when the color mode is changed and this addon supports it very well.
- Install dependencies
yarn add -D storybook-dark-mode
- Integrate the addon and configure directory aliases in
.storybook/main.js
module.exports = { ... addons: [ ... 'storybook-dark-mode', ], webpackFinal: async (config) => { // use tailwind via postcss-loader config.module.rules.push({ test: /\.css$/, use: ['postcss-loader'], include: path.resolve(__dirname, '../'), }); // define aliases for each directory not provided by Next.js by default config.resolve.alias = { ...config.resolve.alias, components: path.resolve(__dirname, '../components'), providers: path.resolve(__dirname, '../providers'), }; return config; }, ... };
- Create
.storybook/constants.js
and put the dark theme color configuration here. We need to share this configuration in multiple files in the next step.1import { themes } from '@storybook/theming'; 2 3export const darkTheme = { 4 ...themes.dark, 5 appBg: '#0f172a', 6 appContentBg: '#0f172a', 7 barBg: '#0f172a', 8};
I use Tailwind’s
bg-slate-900
(#0f172a
) as background color for dark mode. - Create
.storybook/DocsContainer.tsx
as a custom docs container1import React from 'react'; 2 3import { 4 DocsContainer as BaseContainer, 5 DocsContainerProps, 6} from '@storybook/addon-docs/blocks'; 7import { themes } from '@storybook/theming'; 8import { useDarkMode } from 'storybook-dark-mode'; 9 10import { darkTheme } from './constants'; 11// Thanks to https://github.com/hipstersmoothie/storybook-dark-mode/issues/127#issuecomment-1070524402 12export function DocsContainer({ context, ...props }: DocsContainerProps) { 13 const dark = useDarkMode(); 14 15 const finalProps = { 16 ...props, 17 context: { 18 ...context, 19 storyById: (id) => { 20 const storyContext = context.storyById(id); 21 return { 22 ...storyContext, 23 parameters: { 24 ...storyContext?.parameters, 25 docs: { 26 ...storyContext?.parameters?.docs, 27 theme: dark ? darkTheme : themes.light, 28 }, 29 }, 30 }; 31 }, 32 }, 33 }; 34 35 return <BaseContainer {...finalProps} />; 36}
For the time being,
storybook-dark-mode
addon can’t automatically apply dark mode colors into docs view, so we need this custom container to leverageuseDarkMode
hook and change the color of docs based on the active color theme. Thanks to the community for providing this solution. - Update
.storybook/preview.js
to configure dark mode and custom docs container1 2// add the following imports after the existing imports 3import { darkTheme } from './constants'; 4import { DocsContainer } from './DocsContainer'; 5 6export const parameters = { 7 ... 8 darkMode: { 9 classTarget: 'html', 10 dark: darkTheme, 11 stylePreview: true, 12 }, 13 docs: { 14 container: DocsContainer, 15 }, 16 ... 17};
Using this configuration, we put
dark
class in the<html>
tag, use the colors we’ve defined earlier, and enable dark mode for canvas and docs view. - Test the configuration by running
yarn storybook
Now we have dark mode configured in the web app and Storybook. Read more on Tailwind dark mode documentation for more information about how to use the dark mode. Have a nice day!