Темная тема стала стандартом де-факто. Ее отсутствие может стать причиной отказа от пользования сайтом. Особенно если на него заходят программисты, которые сплошь и рядом работают в тёмной теме.

Я покажу, как можно просто добавить тёмную тему в React проект. Разберем основные моменты и сделаем всё красиво. Для тех, кто хочет все сразу:

Roadmap

Шаги, которые мы проделаем дальше:

  1. Создадим create-react-app проект.

  2. Добавим контекст, в котором будем хранить текущую тему.

  3. Напишем переключатель для изменения темы.

  4. Объявим переменные для каждой темы, которые будут влиять на стили компонентов.

Подготовка

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. В качестве переключателя можно взять что угодно, делитесь своими компонентами в комментариях! Жду от вас вашего мнения о статье и рассказов о вашем опыте добавления тёмной темы!

Комментарии (3)


  1. bahanov
    23.03.2022 05:34
    +5

    Темная тема стала стандартом де-факто. Ее отсутствие может стать причиной отказа от пользования сайтом. Особенно если на него заходят программисты, которые сплошь и рядом работают в тёмной теме.

    Самое время отказаться от Хабра.


  1. ItzNeviKat
    23.03.2022 09:44
    +1

    Сначала создадим контекст

    Кажется, вы скопировали код с прошлого блока и забыли изменить.

    // src/providers/ThemeProvider.js
    import React from 'react'
    import { ThemeContext, themes } from 'src/contexts/ThemeContext'

    Абсолютный импорт через src? Зачем?


    1. borisyuzhakov Автор
      23.03.2022 15:01

      Спасибо, поправил.
      Согласен, здесь лучше относительные импорты, поскольку это учебный проект.