Тёмная тема в 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *