React 应用程序中主题的抽象。
appDir
useTheme
挂钩查看实时示例,亲自尝试一下。
$ npm install next-themes
# or
$ yarn add next-themes
您需要一个自定义App
才能使用下一个主题。最简单的_app
如下所示:
// pages/_app.js
function MyApp ( { Component , pageProps } ) {
return < Component { ... pageProps } / >
}
export default MyApp
添加深色模式支持需要 2 行代码:
// pages/_app.js
import { ThemeProvider } from 'next-themes'
function MyApp ( { Component , pageProps } ) {
return (
< ThemeProvider >
< Component { ... pageProps } / >
< / ThemeProvider >
)
}
export default MyApp
您需要更新app/layout.jsx
才能使用下一个主题。最简单的layout
如下所示:
// app/layout.jsx
export default function Layout ( { children } ) {
return (
< html >
< head / >
< body > { children } < / body >
< / html >
)
}
添加深色模式支持需要 2 行代码:
// app/layout.jsx
import { ThemeProvider } from 'next-themes'
export default function Layout ( { children } ) {
return (
< html suppressHydrationWarning >
< head / >
< body >
< ThemeProvider > { children } < / ThemeProvider >
< / body >
< / html >
)
}
请注意, ThemeProvider
是客户端组件,而不是服务器组件。
笔记!如果您不将suppressHydrationWarning 添加到您的
<html>
中,您将收到警告,因为next-themes
会更新该元素。此属性仅适用于一层深度,因此它不会阻止其他元素上的水合警告。
就是这样,您的 Next.js 应用程序完全支持深色模式,包括带有prefers-color-scheme
的系统首选项。主题也会在选项卡之间立即同步。默认情况下,next-themes 会修改html
元素上的data-theme
属性,您可以轻松地使用该属性来设置应用程序的样式:
: root {
/* Your default theme */
--background : white;
--foreground : black;
}
[ data-theme = 'dark' ] {
--background : black;
--foreground : white;
}
笔记!如果您将主题提供程序的属性设置为 Tailwind 的类,则下一个主题将修改
html
元素上的class
属性。见顺风。
您的 UI 需要了解当前主题并能够更改它。 useTheme
挂钩提供主题信息:
import { useTheme } from 'next-themes'
const ThemeChanger = ( ) => {
const { theme , setTheme } = useTheme ( )
return (
< div >
The current theme is: { theme }
< button onClick = { ( ) => setTheme ( 'light' ) } > Light Mode < / button >
< button onClick = { ( ) => setTheme ( 'dark' ) } > Dark Mode < / button >
< / div >
)
}
警告!上面的代码是水合不安全的,并且在使用 SSG 或 SSR 渲染时会抛出水合不匹配警告。这是因为我们无法知道服务器上的
theme
,因此在安装到客户端之前它始终是undefined
。您应该延迟渲染任何主题切换 UI,直到安装在客户端上。请参阅示例。
让我们深入了解细节。
您的所有主题配置都会传递给 ThemeProvider。
storageKey = 'theme'
: 用于在 localStorage 中存储主题设置的键defaultTheme = 'system'
:默认主题名称(对于 v0.0.12 及更低版本,默认为light
)。如果enableSystem
为false,则默认主题为light
forcedTheme
:当前页面的强制主题名称(不修改保存的主题设置)enableSystem = true
: 是否根据prefers-color-scheme
在dark
和light
之间切换enableColorScheme = true
:是否向浏览器指示内置 UI(如输入和按钮)使用哪种配色方案(深色或浅色)disableTransitionOnChange = false
:切换主题时可以选择禁用所有 CSS 转换(示例)themes = ['light', 'dark']
: 主题名称列表attribute = 'data-theme'
: 根据活动主题修改的 HTML 属性class
和data-*
(表示任何数据属性、 data-mode
、 data-color
等)(示例)value
:主题名称到属性值的可选映射object
,其中 key 是主题名称,value 是属性值(示例)nonce
:传递给注入script
标签的可选随机数,用于将 CSP 中的下一个主题脚本列入允许列表scriptProps
:传递给注入script
标签的可选道具(示例)useTheme 不带参数,但返回:
theme
: 活动主题名称setTheme(name)
:更新主题的函数。该 API 与useState
-hook 返回的 set 函数相同。传递新主题值或使用回调根据当前主题设置新主题。forcedTheme
:强制页面主题或虚假主题。如果设置了forcedTheme
,则应禁用任何主题切换UIresolvedTheme
:如果enableSystem
为true并且活动主题为“system”,则返回系统首选项是否解析为“dark”或“light”。否则,与theme
相同systemTheme
:如果enableSystem
为true,则表示系统主题首选项(“深色”或“浅色”),无论活动主题是什么themes
:传递给ThemeProvider
的主题列表(如果enableSystem
为 true,则附加“system”)还不错吧?让我们通过示例看看如何使用这些属性:
实时示例显示了正在运行的下一个主题,包括深色、浅色、系统主题和带有强制主题的页面。
对于 v0.0.12 以上的版本, defaultTheme
自动设置为“system”,因此要使用系统首选项,您只需使用:
< ThemeProvider >
如果您不需要系统主题,请通过enableSystem
禁用它:
< ThemeProvider enableSystem = { false } >
如果您的 Next.js 应用程序使用类根据主题设置页面样式,请将属性 prop 更改为class
:
< ThemeProvider attribute = "class" >
现在,将主题设置为“dark”将会在html
元素上设置class="dark"
。
假设您的酷炫新营销页面仅采用深色模式。页面应始终使用深色主题,并且更改主题不应产生任何效果。要在 Next.js 页面上强制使用主题,只需在页面组件上设置一个变量:
// pages/awesome-page.js
const Page = ( ) => { ... }
Page . theme = 'dark'
export default Page
在您的_app
中,读取变量并将其传递给 ThemeProvider:
function MyApp ( { Component , pageProps } ) {
return (
< ThemeProvider forcedTheme = { Component . theme || null } >
< Component { ... pageProps } / >
< / ThemeProvider >
)
}
完毕!您的页面始终是深色主题(无论用户偏好如何),并且从useTheme
调用setTheme
现在是无操作。但是,您应该确保禁用任何通常会更改主题的 UI:
const { forcedTheme } = useTheme ( )
// Theme is forced, we shouldn't allow user to change the theme
const disabled = ! ! forcedTheme
我在这里写了关于这种技术的文章。我们可以在主题更改之前强制禁用所有 CSS 转换,并在主题更改后立即重新启用它们。这可以确保具有不同过渡持续时间的 UI 在更改主题时不会感到不一致。
要启用此行为,请传递disableTransitionOnChange
属性:
< ThemeProvider disableTransitionOnChange >
活动主题的名称用作 localStorage 值和 DOM 属性的值。如果主题名称为“pink”,localStorage 将包含theme=pink
且 DOM 将为data-theme="pink"
。您无法修改 localStorage 值,但可以修改 DOM 值。
如果我们希望 DOM 在主题为“pink”时渲染data-theme="my-pink-theme"
,请传递value
属性:
< ThemeProvider value = { { pink : 'my-pink-theme' } } >
完毕!需要特别说明的是,这仅影响 DOM。所有值的外观如下:
const { theme } = useTheme ( )
// => "pink"
localStorage . getItem ( 'theme' )
// => "pink"
document . documentElement . getAttribute ( 'data-theme' )
// => "my-pink-theme"
Rocket Loader 是一项 Cloudflare 优化,可推迟内联和外部脚本的加载,以优先考虑网站内容。由于下一个主题依赖于脚本注入来避免页面加载时屏幕闪烁,Rocket Loader 破坏了此功能。通过将data-cfasync="false"
属性添加到脚本标记可以忽略单个脚本:
< ThemeProvider scriptProps = { { 'data-cfasync' : 'false' } } >
next-themes 旨在支持任意数量的主题!只需传递主题列表:
< ThemeProvider themes = { [ 'pink' , 'red' , 'blue' ] } >
笔记!当您传递
themes
时,默认的主题集(“浅色”和“深色”)将被覆盖。如果您仍然想要浅色和深色主题,请确保包含这些主题:
< ThemeProvider themes = { [ 'pink' , 'red' , 'blue' , 'light' , 'dark' ] } >
有关如何使用它的示例,请查看多主题示例
该库不依赖于使用 CSS 变量的主题样式。您可以在 CSS 中对值进行硬编码,一切都会按预期工作(没有任何闪烁):
html ,
body {
color : # 000 ;
background : # fff ;
}
[ data-theme = 'dark' ] ,
[ data-theme = 'dark' ] body {
color : # fff ;
background : # 000 ;
}
Next Themes 完全独立于 CSS,它可以与任何库一起使用。例如,使用样式组件,您只需要在自定义应用程序中createGlobalStyle
:
// pages/_app.js
import { createGlobalStyle } from 'styled-components'
import { ThemeProvider } from 'next-themes'
// Your themeing variables
const GlobalStyle = createGlobalStyle `
:root {
--fg: #000;
--bg: #fff;
}
[data-theme="dark"] {
--fg: #fff;
--bg: #000;
}
`
function MyApp ( { Component , pageProps } ) {
return (
< >
< GlobalStyle / >
< ThemeProvider >
< Component { ... pageProps } / >
< / ThemeProvider >
< / >
)
}
因为我们无法知道服务器上的theme
,所以从useTheme
返回的许多值在安装到客户端之前都是undefined
。这意味着如果您在安装到客户端之前尝试根据当前主题渲染 UI,您将看到水合不匹配错误。
以下代码示例是不安全的:
import { useTheme } from 'next-themes'
// Do NOT use this! It will throw a hydration mismatch error.
const ThemeSwitch = ( ) => {
const { theme , setTheme } = useTheme ( )
return (
< select value = { theme } onChange = { e => setTheme ( e . target . value ) } >
< option value = "system" > System < / option >
< option value = "dark" > Dark < / option >
< option value = "light" > Light < / option >
< / select >
)
}
export default ThemeSwitch
要解决此问题,请确保在页面安装在客户端上时仅呈现使用当前主题的 UI:
import { useState , useEffect } from 'react'
import { useTheme } from 'next-themes'
const ThemeSwitch = ( ) => {
const [ mounted , setMounted ] = useState ( false )
const { theme , setTheme } = useTheme ( )
// useEffect only runs on the client, so now we can safely show the UI
useEffect ( ( ) => {
setMounted ( true )
} , [ ] )
if ( ! mounted ) {
return null
}
return (
< select value = { theme } onChange = { e => setTheme ( e . target . value ) } >
< option value = "system" > System < / option >
< option value = "dark" > Dark < / option >
< option value = "light" > Light < / option >
< / select >
)
}
export default ThemeSwitch
或者,您可以在客户端延迟加载组件。以下示例使用next/dynamic
但您也可以使用React.lazy
:
import dynamic from 'next/dynamic'
const ThemeSwitch = dynamic ( ( ) => import ( './ThemeSwitch' ) , { ssr : false } )
const ThemePage = ( ) => {
return (
< div >
< ThemeSwitch / >
< / div >
)
}
export default ThemePage
为了避免布局转换,请考虑渲染骨架/占位符,直到安装在客户端上。
根据当前主题显示不同的图像也会遇到水合作用不匹配的问题。使用next/image
您可以使用空图像,直到主题得到解决:
import Image from 'next/image'
import { useTheme } from 'next-themes'
function ThemedImage ( ) {
const { resolvedTheme } = useTheme ( )
let src
switch ( resolvedTheme ) {
case 'light' :
src = '/light.png'
break
case 'dark' :
src = '/dark.png'
break
default :
src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'
break
}
return < Image src = { src } width = { 400 } height = { 400 } / >
}
export default ThemedImage
您还可以使用 CSS 根据当前主题隐藏或显示内容。为了避免水合不匹配,您需要渲染两个版本的 UI,并使用 CSS 隐藏未使用的版本。例如:
function ThemedImage ( ) {
return (
< >
{ /* When the theme is dark, hide this div */ }
< div data-hide-on-theme = "dark" >
< Image src = "light.png" width = { 400 } height = { 400 } / >
< / div >
{ /* When the theme is light, hide this div */ }
< div data-hide-on-theme = "light" >
< Image src = "dark.png" width = { 400 } height = { 400 } / >
< / div >
< / >
)
}
export default ThemedImage
[ data-theme = 'dark' ] [ data-hide-on-theme = 'dark' ] ,
[ data-theme = 'light' ] [ data-hide-on-theme = 'light' ] {
display : none;
}
访问实时示例 • 查看示例源代码
笔记! Tailwind 仅在版本 >2 中支持深色模式。
在tailwind.config.js
中,将暗模式属性设置为selector
:
// tailwind.config.js
module . exports = {
darkMode : 'selector'
}
注意:如果您使用的是旧版本的 tailwindcss < 3.4.1,请使用'class'
而不是'selector'
将主题提供程序的属性设置为 class:
// pages/_app.tsx
< ThemeProvider attribute = "class" >
如果您使用 value 属性指定不同的属性值,请确保您的深色主题按照 Tailwind 的要求明确使用“dark”值。
就是这样!现在您可以使用暗模式特定类:
< h1 className = "text-black dark:text-white" >
从 v3.4.1 开始,Tailwind 还允许您为暗模式使用自定义选择器。
在这种情况下,您的tailwind.config.js
将如下所示:
// tailwind.config.js
module . exports = {
// data-mode is used as an example, next-themes supports using any data attribute
darkMode : [ 'selector' , '[data-mode="dark"]' ]
…
}
现在将 ThemeProvider 的属性设置为data-mode
:
// pages/_app.tsx
< ThemeProvider attribute = "data-mode" >
通过此设置,您现在可以使用 Tailwind 的深色模式类,如前面的示例所示:
ThemeProvider 会自动将脚本注入next/head
以便在加载页面的其余部分之前使用正确的属性更新html
元素。这意味着页面在任何情况下都不会闪烁,包括强制主题、系统主题、多个主题和隐身。不需要noflash.js
。
为什么我的页面仍然闪烁?
在 Next.js 开发模式下,页面可能仍会闪烁。当您在生产模式下构建应用程序时,不会出现闪烁。
为什么会出现服务器/客户端不匹配错误?
使用useTheme
时,在渲染依赖于当前主题的 UI 时,您将看到水合不匹配错误。这是因为useTheme
返回的许多值在服务器上未定义,因为在安装到客户端之前我们无法读取localStorage
。请参阅示例了解如何修复此错误。
我需要在这个库中使用 CSS 变量吗?
没有。请参阅示例。
我可以在主体或其他元素上设置类或数据属性吗?
没有。如果您有充分的理由支持此功能,请提出问题。
我可以将此软件包与 Gatsby 或 CRA 一起使用吗?
是的,从0.3.0版本开始。
注入的脚本是否缩小了?
是的。
为什么需要resolvedTheme
?
当支持系统主题首选项时,您需要确保这反映在您的 UI 中。这意味着当系统主题首选项处于活动状态时,您的按钮、选择、下拉列表或用于指示当前主题的任何内容都应显示“系统”。
如果我们不区分theme
和resolvedTheme
,UI 将显示“Dark”或“Light”,而实际上它应该是“System”。
然后, resolvedTheme
对于在运行时修改行为或样式很有用:
const { resolvedTheme } = useTheme ( )
< div style = { { color : resolvedTheme === 'dark' ? 'white' : 'black' } } >
如果我们没有resolvedTheme
并且只使用了theme
,你会丢失有关UI状态的信息(你只会知道主题是“系统”,而不是它解析的内容)。