Dark Theme in Nextjs without Flicker using Tailwind
Tailwind provides us an easy way to switch between light and dark themes. Sometimes, a white flash appears when you're switching between the themes, also knows as a page flicker. To avoid this you should identify the system them of a user at load time.
Understanding the Flicker Problem
Next.js, a popular framework for building React applications, pre-renders pages on the server side, making websites fast and snappy. However, it can't predict user preferences like whether someone prefers light or dark mode. So, when a user manually toggles between themes, there can be a brief moment where the wrong theme is applied, causing that unwanted flash.
The next-themes package can help avoid this issue while allowing users the ability to toggle between light and dark mode in your Next.js website.
On this website I applied next-themes
with TailwindCSS
following the below steps:
Change your tailwind config
Go to your tailwind.config.ts
file and add a darkMode
setting. This is for toggling between dark and light modes manually.
import type { Config } from 'tailwindcss'
const config: Config = {
content: [
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
'./src/app/**/*.{js,ts,jsx,tsx,mdx}',
],
theme: {
//....
},
plugins: [
require('@tailwindcss/typography'),
],
darkMode: 'class',
}
export default config
import type { Config } from 'tailwindcss'
const config: Config = {
content: [
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
'./src/app/**/*.{js,ts,jsx,tsx,mdx}',
],
theme: {
//....
},
plugins: [
require('@tailwindcss/typography'),
],
darkMode: 'class',
}
export default config
Installation of next-themes
next-themes
is a theme React library designed for Nextjs applications. Managing themes with this application is easier as it helps prevent the flicker problem by using Nextjs's capabilities for server-side rendering and client-side hydration.
To install next-themes use the command:
npm install next-themes
npm install next-themes
Setup and usage of next-themes
Let's create a file Providers.tsx
under your components:
'use client'
import { ThemeProvider } from "next-themes"
export function Providers({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
>
{children}
</ThemeProvider>
)
}
'use client'
import { ThemeProvider } from "next-themes"
export function Providers({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
>
{children}
</ThemeProvider>
)
}
Now, we need to add <Providers>
component to your layout.tsx
file, inside the <body>
tag. Since the <html>
tag is being updated by next-themes, we get a hydration warning in the console. To suppress this you can set suppressHydrationWarning as true
.
import "@/styles/main.css"
import type { Metadata } from 'next'
import { Providers } from "@/components/Providers"
import Navbar from "@/components/Navbar"
import Footer from "@/components/Footer"
export const metadata: Metadata = {
// ....
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html suppressHydrationWarning={true} lang="en">
<body className="dark:bg-dark-1 bg-white">
<Providers>
<Navbar />
{children}
<Footer />
</Providers>
</body>
</html>
)
}
import "@/styles/main.css"
import type { Metadata } from 'next'
import { Providers } from "@/components/Providers"
import Navbar from "@/components/Navbar"
import Footer from "@/components/Footer"
export const metadata: Metadata = {
// ....
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html suppressHydrationWarning={true} lang="en">
<body className="dark:bg-dark-1 bg-white">
<Providers>
<Navbar />
{children}
<Footer />
</Providers>
</body>
</html>
)
}
Switch theme from the UI
Icons for switching the theme
You can use your custom icons, or if you're feeling lazy feel free to use the icons I've used. The icons I use are defined in Icons.tsx file. We need two icons, one for dark and one for light mode:
export function DarkModeIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" fill="currentColor" strokeLinecap="round" strokeLinejoin="round" {...props}>
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M12 1.992a10 10 0 1 0 9.236 13.838c.341 -.82 -.476 -1.644 -1.298 -1.31a6.5 6.5 0 0 1 -6.864 -10.787l.077 -.08c.551 -.63 .113 -1.653 -.758 -1.653h-.266l-.068 -.006l-.06 -.002z" strokeWidth="0" fill="currentColor" />
</svg>
)
}
export function LightModeIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" fill="currentColor" strokeLinecap="round" strokeLinejoin="round" {...props}>
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M12 19a1 1 0 0 1 .993 .883l.007 .117v1a1 1 0 0 1 -1.993 .117l-.007 -.117v-1a1 1 0 0 1 1 -1z" strokeWidth="0" />
<path d="M18.313 16.91l.094 .083l.7 .7a1 1 0 0 1 -1.32 1.497l-.094 -.083l-.7 -.7a1 1 0 1 1.218 -1.567l.102 .07z" strokeWidth="0" />
<path d="M7.007 16.993a1 1 0 0 1 .083 1.32l-.083 .094l-.7 .7a1 1 0 0 1 -1.497 -1.32l.083 -.094l.7 -.7a1 1 0 0 1 1.414 0z" strokeWidth="0" />
<path d="M4 11a1 1 0 0 1 .117 1.993l-.117 .007h-1a1 1 0 0 1 -.117 -1.993l.117 -.007h1z" strokeWidth="0" />
<path d="M21 11a1 1 0 0 1 .117 1.993l-.117 .007h-1a1 1 0 0 1 -.117 -1.993l.117 -.007h1z" strokeWidth="0" />
<path d="M6.213 4.81l.094 .083l.7 .7a1 1 0 0 1 -1.32 1.497l-.094 -.083l-.7 -.7a1 1 0 0 1 1.217 -1.567l.102 .07z" strokeWidth="0" />
<path d="M19.107 4.893a1 1 0 0 1 .083 1.32l-.083 .094l-.7 .7a1 1 0 0 1 -1.497 -1.32l.083 -.094l.7 -.7a1 1 0 0 1 1.414 0z" strokeWidth="0" />
<path d="M12 2a1 1 0 0 1 .993 .883l.007 .117v1a1 1 0 0 1 -1.993 .117l-.007 -.117v-1a1 1 0 0 1 1 -1z" strokeWidth="0" />
<path d="M12 7a5 5 0 1 1 -4.995 5.217l-.005 -.217l.005 -.217a5 5 0 0 1 4.995 -4.783z" strokeWidth="0" />
</svg>
)
}
export function DarkModeIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" fill="currentColor" strokeLinecap="round" strokeLinejoin="round" {...props}>
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M12 1.992a10 10 0 1 0 9.236 13.838c.341 -.82 -.476 -1.644 -1.298 -1.31a6.5 6.5 0 0 1 -6.864 -10.787l.077 -.08c.551 -.63 .113 -1.653 -.758 -1.653h-.266l-.068 -.006l-.06 -.002z" strokeWidth="0" fill="currentColor" />
</svg>
)
}
export function LightModeIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" fill="currentColor" strokeLinecap="round" strokeLinejoin="round" {...props}>
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M12 19a1 1 0 0 1 .993 .883l.007 .117v1a1 1 0 0 1 -1.993 .117l-.007 -.117v-1a1 1 0 0 1 1 -1z" strokeWidth="0" />
<path d="M18.313 16.91l.094 .083l.7 .7a1 1 0 0 1 -1.32 1.497l-.094 -.083l-.7 -.7a1 1 0 1 1.218 -1.567l.102 .07z" strokeWidth="0" />
<path d="M7.007 16.993a1 1 0 0 1 .083 1.32l-.083 .094l-.7 .7a1 1 0 0 1 -1.497 -1.32l.083 -.094l.7 -.7a1 1 0 0 1 1.414 0z" strokeWidth="0" />
<path d="M4 11a1 1 0 0 1 .117 1.993l-.117 .007h-1a1 1 0 0 1 -.117 -1.993l.117 -.007h1z" strokeWidth="0" />
<path d="M21 11a1 1 0 0 1 .117 1.993l-.117 .007h-1a1 1 0 0 1 -.117 -1.993l.117 -.007h1z" strokeWidth="0" />
<path d="M6.213 4.81l.094 .083l.7 .7a1 1 0 0 1 -1.32 1.497l-.094 -.083l-.7 -.7a1 1 0 0 1 1.217 -1.567l.102 .07z" strokeWidth="0" />
<path d="M19.107 4.893a1 1 0 0 1 .083 1.32l-.083 .094l-.7 .7a1 1 0 0 1 -1.497 -1.32l.083 -.094l.7 -.7a1 1 0 0 1 1.414 0z" strokeWidth="0" />
<path d="M12 2a1 1 0 0 1 .993 .883l.007 .117v1a1 1 0 0 1 -1.993 .117l-.007 -.117v-1a1 1 0 0 1 1 -1z" strokeWidth="0" />
<path d="M12 7a5 5 0 1 1 -4.995 5.217l-.005 -.217l.005 -.217a5 5 0 0 1 4.995 -4.783z" strokeWidth="0" />
</svg>
)
}
Create a ThemeSwitcher component
After creating the Provider
you need a ThemeSwitcher
component which will change the theme of the website on press of a button.
'use client'
import { useState, useEffect } from 'react'
import { useTheme } from 'next-themes'
import { DarkModeIcon, LightModeIcon } from './Icons'
export default function ThemeSwitcher() {
const [mounted, setMounted] = useState(false)
const { setTheme, resolvedTheme } = useTheme()
useEffect(() => {
setMounted(true)
}, [])
if (!mounted) {
return null
}
return (
<div className={`theme ${
resolvedTheme === 'light' ?
'light-theme' :
'dark-theme'
}`}>
{resolvedTheme === 'light' ?
<DarkModeIcon onClick={() => setTheme('dark')} /> :
<LightModeIcon onClick={() => setTheme('light')} />
}
</div>
)
}
'use client'
import { useState, useEffect } from 'react'
import { useTheme } from 'next-themes'
import { DarkModeIcon, LightModeIcon } from './Icons'
export default function ThemeSwitcher() {
const [mounted, setMounted] = useState(false)
const { setTheme, resolvedTheme } = useTheme()
useEffect(() => {
setMounted(true)
}, [])
if (!mounted) {
return null
}
return (
<div className={`theme ${
resolvedTheme === 'light' ?
'light-theme' :
'dark-theme'
}`}>
{resolvedTheme === 'light' ?
<DarkModeIcon onClick={() => setTheme('dark')} /> :
<LightModeIcon onClick={() => setTheme('light')} />
}
</div>
)
}
Add it to your NavBar
Now that we have the component in place we need to place it in the UI, let's add it in the NavBar.tsx
file for easy theme switching for the user.
// ....
import ThemeSwitcher from "./ThemeSwitcher"
import { useState } from "react"
export default function Navbar() {
const [navbar, setNavbar] = useState(false);
return (
<div>
<header className="navbar-header">
// ....
<div className="navbar-theme">
<ThemeSwitcher />
</div>
// ...
</header>
</div>
)
}
// ....
import ThemeSwitcher from "./ThemeSwitcher"
import { useState } from "react"
export default function Navbar() {
const [navbar, setNavbar] = useState(false);
return (
<div>
<header className="navbar-header">
// ....
<div className="navbar-theme">
<ThemeSwitcher />
</div>
// ...
</header>
</div>
)
}
You can now switch themes in Next.js without any flicker. If you guys have any questions, feel free to reach out in the comment section!