Абстракция для тем в вашем приложении React.
appDir
Next.js 13.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
— это клиентский компонент, а не серверный компонент.
Примечание! Если вы не добавите подавлениеHydrationWarning в свой
<html>
вы получите предупреждения, посколькуnext-themes
обновляет этот элемент. Это свойство применяется только на один уровень глубины, поэтому оно не блокирует предупреждения о гидратации на других элементах.
Вот и все, ваше приложение Next.js полностью поддерживает темный режим, включая системные настройки с помощью prefers-color-scheme
. Тема также мгновенно синхронизируется между вкладками. По умолчанию next-themes изменяет атрибут data-theme
в элементе html
, который вы можете легко использовать для стилизации своего приложения:
: root {
/* Your default theme */
--background : white;
--foreground : black;
}
[ data-theme = 'dark' ] {
--background : black;
--foreground : white;
}
Примечание! Если вы установите атрибут вашего поставщика тем на класс для Tailwind, то следующие темы изменят атрибут
class
в элементеhtml
. См. «Попутный ветер».
Ваш пользовательский интерфейс должен знать текущую тему и иметь возможность ее изменить. Хук 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
, пока не будет установлена на клиенте.Вам следует отложить рендеринг любого пользовательского интерфейса переключения тем до тех пор, пока он не будет установлен на клиенте. См. пример.
Давайте углубимся в детали.
Вся конфигурация вашей темы передается ThemeProvider.
storageKey = 'theme'
: ключ, используемый для хранения настроек темы в localStorage.defaultTheme = 'system'
: имя темы по умолчанию (для версии 0.0.12 и ниже по умолчанию было light
). Если enableSystem
имеет значение false, тема по умолчанию — light
forcedTheme
: принудительное имя темы для текущей страницы (не изменяет сохраненные настройки темы).enableSystem = true
: следует ли переключаться между dark
и light
в зависимости от prefers-color-scheme
enableColorScheme = true
: указывать ли браузерам, какая цветовая схема используется (темная или светлая) для встроенного пользовательского интерфейса, такого как входы и кнопки.disableTransitionOnChange = false
: опционально отключить все переходы CSS при переключении тем (пример)themes = ['light', 'dark']
: список названий тем.attribute = 'data-theme'
: атрибут HTML изменен на основе активной темы.class
и data-*
(имеется в виду любой атрибут данных, data-mode
, data-color
и т. д.) (пример)value
: Необязательное сопоставление имени темы со значением атрибута.object
, где ключ — это имя темы, а значение — значение атрибута (пример)nonce
: необязательный nonce, передаваемый во внедренный тег script
, используемый для внесения в список разрешенных сценариев следующей темы в вашем CSP.scriptProps
: необязательные реквизиты для передачи в тег внедренного script
(пример)useTheme не принимает параметров, но возвращает:
theme
: название активной темы.setTheme(name)
: Функция для обновления темы. API идентичен функции set, возвращаемой useState
-hook. Передайте новое значение темы или используйте обратный вызов, чтобы установить новую тему на основе текущей темы.forcedTheme
: принудительная тема страницы или ложная. Если установлена forcedTheme
, вам следует отключить любой пользовательский интерфейс переключения тем.resolvedTheme
: если enableSystem
имеет значение true и активная тема — «системная», это возвращает значение, разрешено ли системное предпочтение как «темное» или «светлое». В остальном идентично theme
systemTheme
: если enableSystem
имеет значение true, представляет предпочтение системной темы («темная» или «светлая»), независимо от того, какая тема активна.themes
: список тем, передаваемых в ThemeProvider
(с добавлением слова «system», если enableSystem
имеет значение true).Не так уж и плохо, правда? Давайте посмотрим, как использовать эти свойства на примерах:
Живой пример показывает следующие темы в действии: темные, светлые, системные темы и страницы с принудительными темами.
Для версий выше v0.0.12 для defaultTheme
автоматически устанавливается значение «system», поэтому для использования системных настроек вы можете просто использовать:
< ThemeProvider >
Если вам не нужна системная тема, отключите ее с помощью enableSystem
:
< ThemeProvider enableSystem = { false } >
Если ваше приложение Next.js использует класс для стилизации страницы на основе темы, измените свойство атрибута на class
:
< ThemeProvider attribute = "class" >
Теперь, установив тему «темная», для элемента 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 >
)
}
Сделанный! Ваша страница всегда имеет темную тему (независимо от предпочтений пользователя), и вызов setTheme
из useTheme
теперь невозможен. Однако вам следует обязательно отключить любой элемент пользовательского интерфейса, который обычно меняет тему:
const { forcedTheme } = useTheme ( )
// Theme is forced, we shouldn't allow user to change the theme
const disabled = ! ! forcedTheme
Об этой методике я писал здесь. Мы можем принудительно отключить все переходы CSS до изменения темы и сразу же после этого снова включить их. Это гарантирует, что ваш пользовательский интерфейс с разной длительностью перехода не будет выглядеть нестабильным при смене темы.
Чтобы включить это поведение, передайте свойство disableTransitionOnChange
:
< ThemeProvider disableTransitionOnChange >
Имя активной темы используется как значение localStorage, так и значение атрибута DOM. Если имя темы «розовое», localStorage будет содержать theme=pink
, а DOM будет data-theme="pink"
. Вы не можете изменить значение localStorage, но можете изменить значение DOM.
Если мы хотим, чтобы DOM вместо этого отображал 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, которая откладывает загрузку встроенных и внешних скриптов для определения приоритета содержимого веб-сайта. Поскольку next-themes использует внедрение скрипта, чтобы избежать мигания экрана при загрузке страницы, 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
пока не будут установлены на клиенте. Это означает, что если вы попытаетесь отобразить пользовательский интерфейс на основе текущей темы перед установкой на клиенте, вы увидите ошибку несоответствия гидратации.
Следующий пример кода небезопасен :
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
Чтобы это исправить, убедитесь, что вы отображаете только пользовательский интерфейс, использующий текущую тему, когда страница монтируется на клиенте:
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, чтобы скрыть или показать контент на основе текущей темы. Чтобы избежать несоответствия гидратации, вам необходимо отобразить обе версии пользовательского интерфейса, при этом 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.
Вот и все! Теперь вы можете использовать специальные классы темного режима:
< h1 className = "text-black dark:text-white" >
Tailwind также позволяет вам использовать собственный селектор для темного режима, начиная с версии 3.4.1.
В этом случае ваш 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
вы увидите ошибку несоответствия гидратации при рендеринге пользовательского интерфейса, основанного на текущей теме. Это связано с тем, что многие значения, возвращаемые useTheme
не определены на сервере, поскольку мы не можем прочитать localStorage
до тех пор, пока не смонтируем его на клиенте. Посмотрите пример, как исправить эту ошибку.
Нужно ли мне использовать переменные CSS с этой библиотекой?
Неа. См. пример.
Могу ли я установить атрибут класса или данных в теле или другом элементе?
Неа. Если у вас есть веская причина для поддержки этой функции, откройте проблему.
Могу ли я использовать этот пакет с Gatsby или CRA?
Да, начиная с версии 0.3.0.
Минимизирован ли внедренный скрипт?
Да.
Зачем нужна resolvedTheme
?
Поддерживая настройку системной темы, вы должны убедиться, что это отражено в вашем пользовательском интерфейсе. Это означает, что ваши кнопки, элементы выбора, раскрывающиеся списки или что-либо еще, что вы используете для обозначения текущей темы, должны иметь надпись «Система», когда настройка темы «Система» активна.
Если бы мы не делали различия между theme
и resolvedTheme
, в пользовательском интерфейсе отображалось бы «Темное» или «Светлое», тогда как на самом деле должно быть «Системное».
resolvedTheme
затем полезно для изменения поведения или стилей во время выполнения:
const { resolvedTheme } = useTheme ( )
< div style = { { color : resolvedTheme === 'dark' ? 'white' : 'black' } } >
Если бы у нас не было resolvedTheme
и мы использовали только theme
, вы бы потеряли информацию о состоянии вашего пользовательского интерфейса (вы бы знали только, что тема является «системной», а не то, к чему она разрешена).