Perfect Next.js dark mode in 2 lines of code. Support System preference and any other theme with no flashing
Installations
npm install next-themes
Score
84
Supply Chain
92.2
Quality
91.3
Maintenance
100
Vulnerability
100
License
Developer
Developer Guide
Module System
CommonJS, ESM
Min. Node Version
Typescript Support
Yes
Node Version
20.11.0
NPM Version
10.2.4
Statistics
5,242 Stars
119 Commits
192 Forks
13 Watching
10 Branches
31 Contributors
Updated on 28 Nov 2024
Bundle Size
3.47 kB
Minified
1.52 kB
Minified + Gzipped
Languages
TypeScript (100%)
Total Downloads
Cumulative downloads
Total Downloads
42,789,745
Last day
1.5%
172,921
Compared to previous day
Last week
9.5%
1,023,750
Compared to previous week
Last month
13.3%
4,050,977
Compared to previous month
Last year
264.3%
30,614,207
Compared to previous year
Daily Downloads
Weekly Downloads
Monthly Downloads
Yearly Downloads
next-themes
An abstraction for themes in your React app.
- ✅ Perfect dark mode in 2 lines of code
- ✅ System setting with prefers-color-scheme
- ✅ Themed browser UI with color-scheme
- ✅ Support for Next.js 13
appDir
- ✅ No flash on load (both SSR and SSG)
- ✅ Sync theme across tabs and windows
- ✅ Disable flashing when changing themes
- ✅ Force pages to specific themes
- ✅ Class or data attribute selector
- ✅
useTheme
hook
Check out the Live Example to try it for yourself.
Install
1$ npm install next-themes 2# or 3$ yarn add next-themes
Use
With pages/
You'll need a Custom App
to use next-themes. The simplest _app
looks like this:
1// pages/_app.js 2 3function MyApp({ Component, pageProps }) { 4 return <Component {...pageProps} /> 5} 6 7export default MyApp
Adding dark mode support takes 2 lines of code:
1// pages/_app.js 2import { ThemeProvider } from 'next-themes' 3 4function MyApp({ Component, pageProps }) { 5 return ( 6 <ThemeProvider> 7 <Component {...pageProps} /> 8 </ThemeProvider> 9 ) 10} 11 12export default MyApp
With app/
You'll need to update your app/layout.jsx
to use next-themes. The simplest layout
looks like this:
1// app/layout.jsx 2export default function Layout({ children }) { 3 return ( 4 <html> 5 <head /> 6 <body>{children}</body> 7 </html> 8 ) 9}
Adding dark mode support takes 2 lines of code:
1// app/layout.jsx 2import { ThemeProvider } from 'next-themes' 3 4export default function Layout({ children }) { 5 return ( 6 <html suppressHydrationWarning> 7 <head /> 8 <body> 9 <ThemeProvider>{children}</ThemeProvider> 10 </body> 11 </html> 12 ) 13}
Note that ThemeProvider
is a client component, not a server component.
Note! If you do not add suppressHydrationWarning to your
<html>
you will get warnings becausenext-themes
updates that element. This property only applies one level deep, so it won't block hydration warnings on other elements.
HTML & CSS
That's it, your Next.js app fully supports dark mode, including System preference with prefers-color-scheme
. The theme is also immediately synced between tabs. By default, next-themes modifies the data-theme
attribute on the html
element, which you can easily use to style your app:
1:root { 2 /* Your default theme */ 3 --background: white; 4 --foreground: black; 5} 6 7[data-theme='dark'] { 8 --background: black; 9 --foreground: white; 10}
Note! If you set the attribute of your Theme Provider to class for Tailwind next-themes will modify the
class
attribute on thehtml
element. See With Tailwind.
useTheme
Your UI will need to know the current theme and be able to change it. The useTheme
hook provides theme information:
1import { useTheme } from 'next-themes' 2 3const ThemeChanger = () => { 4 const { theme, setTheme } = useTheme() 5 6 return ( 7 <div> 8 The current theme is: {theme} 9 <button onClick={() => setTheme('light')}>Light Mode</button> 10 <button onClick={() => setTheme('dark')}>Dark Mode</button> 11 </div> 12 ) 13}
Warning! The above code is hydration unsafe and will throw a hydration mismatch warning when rendering with SSG or SSR. This is because we cannot know the
theme
on the server, so it will always beundefined
until mounted on the client.You should delay rendering any theme toggling UI until mounted on the client. See the example.
API
Let's dig into the details.
ThemeProvider
All your theme configuration is passed to ThemeProvider.
storageKey = 'theme'
: Key used to store theme setting in localStoragedefaultTheme = 'system'
: Default theme name (for v0.0.12 and lower the default waslight
). IfenableSystem
is false, the default theme islight
forcedTheme
: Forced theme name for the current page (does not modify saved theme settings)enableSystem = true
: Whether to switch betweendark
andlight
based onprefers-color-scheme
enableColorScheme = true
: Whether to indicate to browsers which color scheme is used (dark or light) for built-in UI like inputs and buttonsdisableTransitionOnChange = false
: Optionally disable all CSS transitions when switching themes (example)themes = ['light', 'dark']
: List of theme namesattribute = 'data-theme'
: HTML attribute modified based on the active theme- accepts
class
anddata-*
(meaning any data attribute,data-mode
,data-color
, etc.) (example)
- accepts
value
: Optional mapping of theme name to attribute value- value is an
object
where key is the theme name and value is the attribute value (example)
- value is an
nonce
: Optional nonce passed to the injectedscript
tag, used to allow-list the next-themes script in your CSPscriptProps
: Optional props to pass to the injectedscript
tag (example)
useTheme
useTheme takes no parameters, but returns:
theme
: Active theme namesetTheme(name)
: Function to update the theme. The API is identical to the set function returned byuseState
-hook. Pass the new theme value or use a callback to set the new theme based on the current theme.forcedTheme
: Forced page theme or falsy. IfforcedTheme
is set, you should disable any theme switching UIresolvedTheme
: IfenableSystem
is true and the active theme is "system", this returns whether the system preference resolved to "dark" or "light". Otherwise, identical totheme
systemTheme
: IfenableSystem
is true, represents the System theme preference ("dark" or "light"), regardless what the active theme isthemes
: The list of themes passed toThemeProvider
(with "system" appended, ifenableSystem
is true)
Not too bad, right? Let's see how to use these properties with examples:
Examples
The Live Example shows next-themes in action, with dark, light, system themes and pages with forced themes.
Use System preference by default
For versions above v0.0.12, the defaultTheme
is automatically set to "system", so to use System preference you can simply use:
1<ThemeProvider>
Ignore System preference
If you don't want a System theme, disable it via enableSystem
:
1<ThemeProvider enableSystem={false}>
Class instead of data attribute
If your Next.js app uses a class to style the page based on the theme, change the attribute prop to class
:
1<ThemeProvider attribute="class">
Now, setting the theme to "dark" will set class="dark"
on the html
element.
Force page to a theme
Let's say your cool new marketing page is dark mode only. The page should always use the dark theme, and changing the theme should have no effect. To force a theme on your Next.js pages, simply set a variable on the page component:
1// pages/awesome-page.js 2 3const Page = () => { ... } 4Page.theme = 'dark' 5export default Page
In your _app
, read the variable and pass it to ThemeProvider:
1function MyApp({ Component, pageProps }) { 2 return ( 3 <ThemeProvider forcedTheme={Component.theme || null}> 4 <Component {...pageProps} /> 5 </ThemeProvider> 6 ) 7}
Done! Your page is always dark theme (regardless of user preference), and calling setTheme
from useTheme
is now a no-op. However, you should make sure to disable any of your UI that would normally change the theme:
1const { forcedTheme } = useTheme() 2 3// Theme is forced, we shouldn't allow user to change the theme 4const disabled = !!forcedTheme
Disable transitions on theme change
I wrote about this technique here. We can forcefully disable all CSS transitions before the theme is changed, and re-enable them immediately afterwards. This ensures your UI with different transition durations won't feel inconsistent when changing the theme.
To enable this behavior, pass the disableTransitionOnChange
prop:
1<ThemeProvider disableTransitionOnChange>
Differing DOM attribute and theme name
The name of the active theme is used as both the localStorage value and the value of the DOM attribute. If the theme name is "pink", localStorage will contain theme=pink
and the DOM will be data-theme="pink"
. You cannot modify the localStorage value, but you can modify the DOM value.
If we want the DOM to instead render data-theme="my-pink-theme"
when the theme is "pink", pass the value
prop:
1<ThemeProvider value={{ pink: 'my-pink-theme' }}>
Done! To be extra clear, this affects only the DOM. Here's how all the values will look:
1const { theme } = useTheme() 2// => "pink" 3 4localStorage.getItem('theme') 5// => "pink" 6 7document.documentElement.getAttribute('data-theme') 8// => "my-pink-theme"
Using with Cloudflare Rocket Loader
Rocket Loader is a Cloudflare optimization that defers the loading of inline and external scripts to prioritize the website content. Since next-themes relies on a script injection to avoid screen flashing on page load, Rocket Loader breaks this functionality. Individual scripts can be ignored by adding the data-cfasync="false"
attribute to the script tag:
1<ThemeProvider scriptProps={{ 'data-cfasync': 'false' }}>
More than light and dark mode
next-themes is designed to support any number of themes! Simply pass a list of themes:
1<ThemeProvider themes={['pink', 'red', 'blue']}>
Note! When you pass
themes
, the default set of themes ("light" and "dark") are overridden. Make sure you include those if you still want your light and dark themes:
1<ThemeProvider themes={['pink', 'red', 'blue', 'light', 'dark']}>
For an example on how to use this, check out the multi-theme example
Without CSS variables
This library does not rely on your theme styling using CSS variables. You can hard-code the values in your CSS, and everything will work as expected (without any flashing):
1html, 2body { 3 color: #000; 4 background: #fff; 5} 6 7[data-theme='dark'], 8[data-theme='dark'] body { 9 color: #fff; 10 background: #000; 11}
With Styled Components and any CSS-in-JS
Next Themes is completely CSS independent, it will work with any library. For example, with Styled Components you just need to createGlobalStyle
in your custom App:
1// pages/_app.js 2import { createGlobalStyle } from 'styled-components' 3import { ThemeProvider } from 'next-themes' 4 5// Your themeing variables 6const GlobalStyle = createGlobalStyle` 7 :root { 8 --fg: #000; 9 --bg: #fff; 10 } 11 12 [data-theme="dark"] { 13 --fg: #fff; 14 --bg: #000; 15 } 16` 17 18function MyApp({ Component, pageProps }) { 19 return ( 20 <> 21 <GlobalStyle /> 22 <ThemeProvider> 23 <Component {...pageProps} /> 24 </ThemeProvider> 25 </> 26 ) 27}
Avoid Hydration Mismatch
Because we cannot know the theme
on the server, many of the values returned from useTheme
will be undefined
until mounted on the client. This means if you try to render UI based on the current theme before mounting on the client, you will see a hydration mismatch error.
The following code sample is unsafe:
1import { useTheme } from 'next-themes' 2 3// Do NOT use this! It will throw a hydration mismatch error. 4const ThemeSwitch = () => { 5 const { theme, setTheme } = useTheme() 6 7 return ( 8 <select value={theme} onChange={e => setTheme(e.target.value)}> 9 <option value="system">System</option> 10 <option value="dark">Dark</option> 11 <option value="light">Light</option> 12 </select> 13 ) 14} 15 16export default ThemeSwitch
To fix this, make sure you only render UI that uses the current theme when the page is mounted on the client:
1import { useState, useEffect } from 'react' 2import { useTheme } from 'next-themes' 3 4const ThemeSwitch = () => { 5 const [mounted, setMounted] = useState(false) 6 const { theme, setTheme } = useTheme() 7 8 // useEffect only runs on the client, so now we can safely show the UI 9 useEffect(() => { 10 setMounted(true) 11 }, []) 12 13 if (!mounted) { 14 return null 15 } 16 17 return ( 18 <select value={theme} onChange={e => setTheme(e.target.value)}> 19 <option value="system">System</option> 20 <option value="dark">Dark</option> 21 <option value="light">Light</option> 22 </select> 23 ) 24} 25 26export default ThemeSwitch
Alternatively, you could lazy load the component on the client side. The following example uses next/dynamic
but you could also use React.lazy
:
1import dynamic from 'next/dynamic' 2 3const ThemeSwitch = dynamic(() => import('./ThemeSwitch'), { ssr: false }) 4 5const ThemePage = () => { 6 return ( 7 <div> 8 <ThemeSwitch /> 9 </div> 10 ) 11} 12 13export default ThemePage
To avoid Layout Shift, consider rendering a skeleton/placeholder until mounted on the client side.
Images
Showing different images based on the current theme also suffers from the hydration mismatch problem. With next/image
you can use an empty image until the theme is resolved:
1import Image from 'next/image' 2import { useTheme } from 'next-themes' 3 4function ThemedImage() { 5 const { resolvedTheme } = useTheme() 6 let src 7 8 switch (resolvedTheme) { 9 case 'light': 10 src = '/light.png' 11 break 12 case 'dark': 13 src = '/dark.png' 14 break 15 default: 16 src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7' 17 break 18 } 19 20 return <Image src={src} width={400} height={400} /> 21} 22 23export default ThemedImage
CSS
You can also use CSS to hide or show content based on the current theme. To avoid the hydration mismatch, you'll need to render both versions of the UI, with CSS hiding the unused version. For example:
1function ThemedImage() { 2 return ( 3 <> 4 {/* When the theme is dark, hide this div */} 5 <div data-hide-on-theme="dark"> 6 <Image src="light.png" width={400} height={400} /> 7 </div> 8 9 {/* When the theme is light, hide this div */} 10 <div data-hide-on-theme="light"> 11 <Image src="dark.png" width={400} height={400} /> 12 </div> 13 </> 14 ) 15} 16 17export default ThemedImage
1[data-theme='dark'] [data-hide-on-theme='dark'], 2[data-theme='light'] [data-hide-on-theme='light'] { 3 display: none; 4}
With TailwindCSS
Visit the live example • View the example source code
NOTE! Tailwind only supports dark mode in version >2.
In your tailwind.config.js
, set the dark mode property to selector
:
1// tailwind.config.js 2module.exports = { 3 darkMode: 'selector' 4}
Note: If you are using an older version of tailwindcss < 3.4.1 use 'class'
instead of 'selector'
Set the attribute for your Theme Provider to class:
1// pages/_app.tsx 2<ThemeProvider attribute="class">
If you're using the value prop to specify different attribute values, make sure your dark theme explicitly uses the "dark" value, as required by Tailwind.
That's it! Now you can use dark-mode specific classes:
1<h1 className="text-black dark:text-white">
Using a custom selector (tailwindcss > 3.4.1)
Tailwind also allows you to use a custom selector for dark-mode as of v3.4.1.
In that case, your tailwind.config.js
would look like this:
1// tailwind.config.js 2module.exports = { 3 // data-mode is used as an example, next-themes supports using any data attribute 4 darkMode: ['selector', '[data-mode="dark"]'] 5 … 6}
Now set the attribute for your ThemeProvider to data-mode
:
1// pages/_app.tsx 2<ThemeProvider attribute="data-mode">
With this setup, you can now use Tailwind's dark mode classes, as in the previous example:
Discussion
The Flash
ThemeProvider automatically injects a script into next/head
to update the html
element with the correct attributes before the rest of your page loads. This means the page will not flash under any circumstances, including forced themes, system theme, multiple themes, and incognito. No noflash.js
required.
FAQ
Why is my page still flashing?
In Next.js dev mode, the page may still flash. When you build your app in production mode, there will be no flashing.
Why do I get server/client mismatch error?
When using useTheme
, you will use see a hydration mismatch error when rendering UI that relies on the current theme. This is because many of the values returned by useTheme
are undefined on the server, since we can't read localStorage
until mounting on the client. See the example for how to fix this error.
Do I need to use CSS variables with this library?
Nope. See the example.
Can I set the class or data attribute on the body or another element?
Nope. If you have a good reason for supporting this feature, please open an issue.
Can I use this package with Gatsby or CRA?
Yes, starting from the 0.3.0 version.
Is the injected script minified?
Yes.
Why is resolvedTheme
necessary?
When supporting the System theme preference, you want to make sure that's reflected in your UI. This means your buttons, selects, dropdowns, or whatever you use to indicate the current theme should say "System" when the System theme preference is active.
If we didn't distinguish between theme
and resolvedTheme
, the UI would show "Dark" or "Light", when it should really be "System".
resolvedTheme
is then useful for modifying behavior or styles at runtime:
1const { resolvedTheme } = useTheme() 2 3<div style={{ color: resolvedTheme === 'dark' ? 'white' : 'black' }}>
If we didn't have resolvedTheme
and only used theme
, you'd lose information about the state of your UI (you would only know the theme is "system", and not what it resolved to).
No vulnerabilities found.
Reason
no dangerous workflow patterns detected
Reason
no binaries found in the repo
Reason
15 commit(s) and 15 issue activity found in the last 90 days -- score normalized to 10
Reason
license file detected
Details
- Info: project has a license file: license.md:0
- Info: FSF or OSI recognized license: MIT License: license.md:0
Reason
Found 19/28 approved changesets -- score normalized to 6
Reason
7 existing vulnerabilities detected
Details
- Warn: Project is vulnerable to: GHSA-grv7-fg5c-xmjg
- Warn: Project is vulnerable to: GHSA-3xgq-45jj-v275
- Warn: Project is vulnerable to: GHSA-952p-6rrq-rcjv
- Warn: Project is vulnerable to: GHSA-gcx4-mw62-g8wm
- Warn: Project is vulnerable to: GHSA-64vr-g452-qvp3
- Warn: Project is vulnerable to: GHSA-9cwx-2883-4wfx
- Warn: Project is vulnerable to: GHSA-3h5v-q93c-6h6q
Reason
detected GitHub workflow tokens with excessive permissions
Details
- Warn: no topLevel permission defined: .github/workflows/e2e.yml:1
- Warn: no topLevel permission defined: .github/workflows/test.yml:1
- Info: no jobLevel write permissions found
Reason
dependency not pinned by hash detected -- score normalized to 0
Details
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/e2e.yml:15: update your workflow using https://app.stepsecurity.io/secureworkflow/pacocoursey/next-themes/e2e.yml/main?enable=pin
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/e2e.yml:18: update your workflow using https://app.stepsecurity.io/secureworkflow/pacocoursey/next-themes/e2e.yml/main?enable=pin
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/e2e.yml:35: update your workflow using https://app.stepsecurity.io/secureworkflow/pacocoursey/next-themes/e2e.yml/main?enable=pin
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/test.yml:14: update your workflow using https://app.stepsecurity.io/secureworkflow/pacocoursey/next-themes/test.yml/main?enable=pin
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/test.yml:17: update your workflow using https://app.stepsecurity.io/secureworkflow/pacocoursey/next-themes/test.yml/main?enable=pin
- Info: 0 out of 5 GitHub-owned GitHubAction dependencies pinned
Reason
no effort to earn an OpenSSF best practices badge detected
Reason
project is not fuzzed
Details
- Warn: no fuzzer integrations found
Reason
security policy file not detected
Details
- Warn: no security policy file detected
- Warn: no security file to analyze
- Warn: no security file to analyze
- Warn: no security file to analyze
Reason
SAST tool is not run on all commits -- score normalized to 0
Details
- Warn: 1 commits out of 21 are checked with a SAST tool
Score
4.7
/10
Last Scanned on 2024-11-18
The Open Source Security Foundation is a cross-industry collaboration to improve the security of open source software (OSS). The Scorecard provides security health metrics for open source projects.
Learn More