Тёмная тема в React с использованием css переменных в scss

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

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

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

- [Репозиторий](https://github.com/walborn/with-dark-theme)

- [Демо](https://with-dark-theme-4ypv8vyi9-walborn.vercel.app/)

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/components/Root/index.js
import React from 'react'

const Root = () => (
	<div>There are will be Dark Theme</div>
)

export default Root

А теперь создадим проводник нашего контекста, в котором сначала получим текущее значение темы, которая хранится в localStorage . Если там еще ничего нет, то берем значение из системной темы. Если и этого нет (привет из виндовс xp) - то устанавливаем темную тему (А что?! Можем себе позволить)

При изменении темы - одновременно сохраняем ее в localStorage

// src/providers/ThemeProvider.js
import React from 'react'
import { ThemeContext, themes } from 'src/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. В качестве переключателя можно взять что угодно, делитесь своими компонентами в комментариях! Жду от вас вашего мнения о статье и рассказов о вашем опыте добавления тёмной темы!

Источник: https://habr.com/ru/post/656995/


Интересные статьи

Интересные статьи

Написание четкого и удобочитаемого кода необходимо для улучшения его качества. Кроме того, чистый код легче тестировать. Нет причин не потратить пять лишних минут на рефа...
При композиции компонентов очень часто возникает задача точечной кастомизации содержимого какого-либо компонента. Например, у нас есть компонент DatePicker, и в разных частях веб-при...
Этот пост будет из серии, об инструментах безопасности, которые доступны в Битриксе сразу «из коробки». Перечислю их все, скажу какой инструмент в какой редакции Битрикса доступен, кратко и не очень р...
Решение задачи распознавания изображений (OCR) сопряжено с различными сложностями. То картинку не получается распознать из-за нестандартной цветовой схемы или из-за искажений. То заказчик хочет р...
Во втором туре выборов губернатора Приморского края 16 сентября 2018 года встречались действующий и.о. губернатора Андрей Тарасенко и занявший второе место в первом туре коммунист Андрей Ищенко. ...