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狀態的資訊(你只知道主題是“系統”,而不是它解析的內容)。