Темная тема стала стандартом де-факто. Ее отсутствие может стать причиной отказа от пользования сайтом. Особенно если на него заходят программисты, которые сплошь и рядом работают в тёмной теме.
Я покажу, как можно просто добавить тёмную тему в React проект. Разберем основные моменты и сделаем всё красиво. Для тех, кто хочет все сразу:
Roadmap
Шаги, которые мы проделаем дальше:
Создадим
create-react-app
проект.Добавим контекст, в котором будем хранить текущую тему.
Напишем переключатель для изменения темы.
Объявим переменные для каждой темы, которые будут влиять на стили компонентов.
Подготовка
1. С помощью cra
создаем проект и сразу добавляем sass
для удобства работы со стилями
> npx create-react-app with-dark-theme
> cd with-dark-theme
> npm i sass -S
2. Удалим ненужные файлы
> cd src
> rm App.css App.js App.test.js index.css logo.svg
3. Создадим удобную структур
# внутри src/
> mkdir -p components/{Root,Toggle} contexts providers
> touch index.scss components/Root/index.js components/Toggle/{index.js,index.module.scss} contexts/ThemeContext.js providers/ThemeProvider.js
Должна получиться такая структура внутри src/
src
├── components
│ ├── Root
│ │ └── index.js
│ └── Toggle
│ ├── index.js
│ └── index.module.scss
├── contexts
│ └── ThemeContext.js
├── providers
│ └── ThemeProvider.js
├── index.js
├── index.scss
└── ...
Поскольку мы внесли изменения в структуру, то немного изменим index.js
// src/index.js
import React from 'react'
import ReactDOM from 'react-dom'
import reportWebVitals from './reportWebVitals'
// теперь корневой компонент у нас не App, а Root
import Root from './components/Root'
// поменяли css на scss
import './index.scss'
ReactDOM.render(
<React.StrictMode>
<Root />
</React.StrictMode>,
document.getElementById('root')
)
// src/components/Root/index.js
import React from 'react'
const Root = () => (
<div>There are will be Dark Theme</div>
)
export default Root
Проект уже запускается, но никакой темной темы пока еще нет.
Давайте добавим ее!
Добавляем контекст
Наполним кодом наши файлы ThemeContext.js
и ThemeProvider.js
.
Сначала объявим контекст.
// src/contexts/ThemeContext.js
import React from 'react'
export const themes = {
dark: 'dark',
light: 'light',
}
export const ThemeContext = React.createContext({})
А далее создадим проводник нашего контекста, в котором сначала получим текущее значение темы, которая хранится в localStorage
. Если там еще ничего нет, то берем значение из системной темы. Если и этого нет (привет из виндовс xp) - то устанавливаем тёмную тему (А что?! Можем себе позволить).
При изменении темы - одновременно сохраняем ее в localStorage
.
// src/providers/ThemeProvider.js
import React from 'react'
import { ThemeContext, themes } from '../contexts/ThemeContext'
const getTheme = () => {
const theme = `${window?.localStorage?.getItem('theme')}`
if (Object.values(themes).includes(theme)) return theme
const userMedia = window.matchMedia('(prefers-color-scheme: light)')
if (userMedia.matches) return themes.light
return themes.dark
}
const ThemeProvider = ({ children }) => {
const [ theme, setTheme ] = React.useState(getTheme)
React.useEffect(() => {
document.documentElement.dataset.theme = theme
localStorage.setItem('theme', theme)
}, [ theme ])
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
)
}
export default ThemeProvider
И теперь зайдем в корневой файл index.js
. Тут мы хотим применить наш ThemeProvider
, которым оборачиваем Root
, чтобы все, что внутри имело доступ к переменной темы.
// src/index.js
import React from 'react'
import ReactDOM from 'react-dom'
import reportWebVitals from './reportWebVitals'
import ThemeProvider from './providers/ThemeProvider' // +
import Root from './components/Root'
import './index.scss'
ReactDOM.render(
<React.StrictMode>
<ThemeProvider>
<Root />
</ThemeProvider>
</React.StrictMode>,
document.getElementById('root')
)
...
Пишем переключатель
Осталось создать переключатель для темы. В нашем случае это будет стандартный тогглер. Ну почти стандартный, мы его немного улучшим, чтобы не совсем грустно было.
// src/components/Toggle/index.js
import React from 'react'
import styles from './index.module.scss'
const Toggle = ({ value, onChange }) => (
<label className={styles.switch} htmlFor="toggler">
<input
id="toggler"
type="checkbox"
onClick={onChange}
checked={value}
readOnly
/>
<span className={styles.slider} />
<span className={styles.wave} />
</label>
)
export default Toggle
// src/components/Toggle/index.module.scss
.root {
position: absolute;
top: 50%;
left: 50%;
width: 120px;
height: 50px;
transform: translate(-50%, -50%);
input {
display: none;
}
.slider {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1;
overflow: hidden;
background-color: #e74a42;
border-radius: 50px;
cursor: pointer;
transition: all 1.4s;
&:before,
&:after {
content: "";
position: absolute;
bottom: 5px;
left: 5px;
width: 40px;
height: 40px;
background-color: #ffffff;
border-radius: 30px;
}
&:before {
transition: 0.4s;
}
&:after {
transition: 0.5s;
}
}
.wave {
position: absolute;
top: 0;
left: 0;
width: 120px;
height: 50px;
border-radius: 40px;
transition: all 1.4s;
&:after {
content: "";
position: absolute;
top: 3px;
left: 20%;
width: 60px;
height: 3px;
background: #ffffff;
border-radius: 100%;
opacity: 0.4;
}
&:before {
content: "";
position: absolute;
top: 10px;
left: 30%;
width: 35px;
height: 2px;
background: #ffffff;
border-radius: 100%;
opacity: 0.3;
}
}
input:checked + .slider {
background-color: transparent;
&:before,
&:after {
transform: translateX(70px);
}
}
input:checked ~ .wave {
display: block;
background-color: #3398d9;
}
}
Почти все! Осталось только добавить наш Toggle
на главную страницу.
// src/components/Root/index.js
import React from 'react'
import { ThemeContext, themes } from '../../contexts/ThemeContext'
import Toggle from '../Toggle'
const Root = () => (
<ThemeContext.Consumer>
{({ theme, setTheme }) => (
<Toggle
onChange={() => {
if (theme === themes.light) setTheme(themes.dark)
if (theme === themes.dark) setTheme(themes.light)
}}
value={theme === themes.dark}
/>
)}
</ThemeContext.Consumer>
)
export default Root
// src/index.scss
:root[data-theme="light"] {
--background-color: #fafafa;
}
:root[data-theme="dark"] {
--background-color: #2b3e51;
}
body {
background: var(--background-color);
}
И чтобы все заработало как надо, нужно задать переменные для каждой темы. Задавать мы их будем через css
переменные, поскольку те переменные, которые используются в scss
нам не подойдут. scss
компилится в css
довольно глупо, он просто подставляет значения переменных во всех местах, где они фигурируют.
Заключение
Внедрить тёмную тему в React оказалось не так уж и сложно. Для этого мы прокидывали информацию о теме с помощью механизма контекстов, который есть в React
. В качестве переключателя можно взять что угодно, делитесь своими компонентами в комментариях! Жду от вас вашего мнения о статье и рассказов о вашем опыте добавления тёмной темы!