- Смена темы сайта через CSS-переменные
- Переключение цветовых тем в React приложении
- Что хотим получить
- Возможность расширения
- Возможность изменить стилизацию приложения (css).
- Возможность изменить стилизацию приложения (js)
- Возможность из любого места приложения инициировать изменение темы
- План действий
- Решения
- Переключение темы
- Связки цветов
- Применение и изменение темы в react приложении.
- Итак, собираем всё вместе:
- Применяем то, что получилось
- Итого
- Summary по решениям:
Смена темы сайта через CSS-переменные
Всем привет. Меня зовут Петр Цой. Я нахожусь в поисках своей первой работы в качестве ReactJS разработчика. Есть хороший опыт самостоятельной разработки коммерческих сайтов. В качестве примера и моего резюме выступает одноименный сайт petrtcoi.com. Ссылка на GitHub.
Если вам нужен начинающий специалист, пишите!
На сайте сейчас кот. В ближайшее время сделаю приличную фотографию и размещу ее.
Возможность переключать сайт между разными цветовыми схемами подразумевает возможность динамически менять стили большинства его компонентов. В минимальном варианте — это цвет текста и цвет заднего фона.
Если округлить, то добиться этого можно двумя путями:
- через JavaScript, меняя свойства соответствующих переменных;
- через CSS, меняя значения CSS-переменных;
Первый вариант требует перерисовки всех компонентов, связанных с новой темой. Это может негативно сказаться на производительности. К тому же, здесь потребуется дополнительная настройка проекта, так как свойства новой темы нужно передавать всем компонентам сайта.
Второй вариант не имеет данного недостатка. Мы просто меняем значения CSS-переменных, отвечающих за отображение темы, и обновление сайта происходит в автоматическом режиме без перерисовки каких-либо компонентов. Этот способ не только быстрее, но и проще в реализации.
На скриншоте ниже показан результат смены темы на моем сайте.
Внизу можно заметить, что один компонент все же переписывался — это собственно сам Switcher, который меняет тему.
Для реализации данной возможности я вынес переменные, связанные с темой в отдельный файл.
Здесь заданы переменные для цвета текста и заднего фона. В базовом варианте — для темной темы, ниже — для светлой темы «light».
Для красоты также добавлена характеристика —change-theme-duration: 0.5s;, чтобы цвета менялись не мгновенно, но с небольшой задержкой.
Рядом, в этой же папке, разместим вспомогательный файл со списком возможных тем. У нас их две.
Далее CSS-переменные темы прописываются в файле с глобальными стилями.
Все, что теперь требуется, так это повесить на наш компонент обработчик, который будет устанавливать на корневой элемент требуемый нам атрибут.
Подобная реализация смены темы размывает зону ответственности. Получается, что часть логики приложения продолжает обрабатываться через JavaScript, а какая-то часть — уже через CSS. Возможно это создаст некоторые трудности при дальнейшем росте проекта. Тем более, если злоупотреблять данным приемом.
С другой стороны, этот метод слишком эффективен, чтобы можно было им пренебречь. Думаю, достаточно будет выполнить следующего:
- Позже к проекту подключу Redux и подобные «глобальные» эффекты будут проводить через него. Все будет аккуратно лежать в одном месте и, по крайней мере, будет просто найти нужный участок кода.
- Подобные CSS-трюки постараюсь ограничить рамками отдельных элементов.
Главное преимущество CSS-подхода привело и к сложности его тестирования. Так как стандартная библиотека для unit-тестов @testing-library попросту не «видит», какие именно параметры скрыты за названиями классов, то она и не в состоянии проверить, какой цвет установлен у элемента, находится ли он в зоне видимости и т.д.
В итоге, для тестирования смены темы я использовал e2e тестирование с помощью Playwright. Это не так удобно, как быстрые unit-тесты, но работает.
Подробнее про тестирование напишу в своей следующей записи.Спасибо за внимание.
Переключение цветовых тем в React приложении
Данный гайд описывает один из возможных подходов к организации фичи темизации приложения.
Глобально статья поделена на 2 части:
Здесь мы опишем логику работы с темами, которая построена на чистом css (ну почти). Но этот механизм не имеет никаких «завязок» на js фреймворк/библиотеку.
В этой части мы посмотрим, как именно в контексте react приложения мы сможем эффеективно применять наши темы.
Что хотим получить
Предет тем, как приступим к решение задачи темизации нашего react приложения сформулируем возможности, которые должна обладать наша будущая система.
Возможность расширения
Мы не должны быть ограничены только одной цветовой темой. И нам не должно быть мучительно больно, если наши старшие братья по интерфейсам (дизайнеры) вдруг запилят к стандартным «светлой» и «тёмной» темам ещё и «малиновую, по случаю дня рождения компании».
Возможность изменить стилизацию приложения (css).
Это в общем то и есть основная цель — мы должны уметь легко применять цвета выбранной темы на наши элементы в css.
Возможность изменить стилизацию приложения (js)
Части наших элементов, может понадобится изменять своё отображение не только на основе css но и на основе свойств, которые мы пробрасываем внутрь компонентов. Например, так работают большинство библиотек элементов (закрыты к изменению, но имеют api для управления).
Следовательно где-то в коде уже наших компонентов мы должны иметь возможность получить значение текущей темы и работать с ним.
Возможность из любого места приложения инициировать изменение темы
В любой момент развития нашего проекта/продукта может изменится дизайн концепция и/или могут тестироваться различные гипотезы, которые могут привести к изменению нашего ui. Компоненты могут меняться местами, элементы управления уезжать «вглубь» или в порталы/модальные окна и т.д.
Для того, чтобы в реализации учесть эти возможные изменения мы должны зафиксировать требование — метод для смены темы должен быть внутри наших компонентов.
При этом мы заранее не можем знать, на какой «глубине/уровне вложенности» этот элемент управления темой может появится.
Кажется теперь, когда мы понимаем все требования к нашей системе, можно переходить к проектированию решения.
P.S.:
В рамках нашего скоупа обсуждения, мы не будем затрагивать вопросы всей дизайн системы, а сосредоточимся только на цветовых темах.
План действий
- Примем решение о том, за счёт чего мы будем переключать цветовую тему
- Определим где и как будем описывать связки цветов. Так же опишем механизм их применения в css
- Сделаем тему доступной внутри react компонентов и определим, как именно мы дадим нашим компонентам возможность тригерить смену цветовой темы
Решения
Переключение темы
Переключать тему мы будем очень просто — изменяя содержимое data-theme атрибута на рутовом элементе (html).
document.documentElement.setAttribute('data-theme', theme);
Значением theme будет строка, являющаяся названием темы: dark | light;
Это максимально простое решение, которое требует минимум действий и так же позволяет нам опираться на каскад css для применения стилей конкретной темы на любом элементе нашей страницы.
Связки цветов
- триады для описание цвета
- [data-theme=’light’] слектор по аттрибуту и custom properties для применения в css.
Цвета для каждой темы будем описывать триадами (сетами по 3 цифры, которые кодируют цвет).
Это удобно для того, чтобы иметь возможность применять прозрачность (устанавливать значение альфа канала) для цветов.
А для применения этих триад будем использовать custom properties. Они широко распространены и используются в том же CRA из коробки.
Т.е. одну и ту же переменную —some-color: 255, 255, 255 мы сможем использовать в двух вариантах: rgb и rgba
При этом нам надо иметь универсальный способ «доставки» значения цвета конкретной темы в тот кусочек css, который мы хотим стилизовать. Для этого будем использовать селектор по аттрибуту, который мы установили в предыдущем пункте, и внутри будем описывать уже переменные:
Итого, общая связка «объявить тему и сделать её доступной в css» будет выглядеть следующим образом:
:root < /* light theme tokens */ --background-primary-light: 44, 191, 170; --accent-light: 37, 89, 88; /* dark theme tokens */ --background-primary-dark: 25, 48, 66; --accent-dark: 90, 182, 204; >/* map tokens to proper theme */ [data-theme='light'] < --background-primary: var(--background-primary-light); --accent: var(--accent-light); >[data-theme='dark']
А непосредственное применение значений этих перменных в css делается в 2 шага:
import './components/theme-provider/themes.css';
Таким образом реализованы базовые требования к нашей системе:
- Возможность изменить стилизацию приложения (css) — просто используем наши custom properties
- Возможность расширения — мы можем создать неограниченное большое количество цветовых тем.
Вот и всё, теперь мы имеем рабочий css механизм переключения цветовой темы. Это база, которая по сути не привязана ни к какому фреймворку/библиотеке и прочим вещам. Только css и его сборка.
Применение и изменение темы в react приложении.
- localStorage для сохранения значения темы
- createContext react контекст, для получения доступа к теме и её изменению на любом уровне вложенности
- useTheme кастомный хук, для упрощения доступа к контексту
Тему надо бы уметь сохранять/получать между перезагрузкой страниц.
const StorageKey = 'features-color-theme'; const getTheme = (): Themes => < let theme = localStorage.getItem(StorageKey); if (!theme) < localStorage.setItem(StorageKey, 'light'); theme = 'light'; >return theme as Themes; >;
Теперь объявим доступные темы и сам контекст:
const supportedThemes = < light: 'light', dark: 'dark', >; type Themes = keyof typeof supportedThemes; const ThemeContext = createContext < | < theme: Themes; setTheme: (theme: Themes) =>void; supportedThemes: < Data theme in css: string >; > | undefined >(undefined);
После этого мы должны создать сам компонент, который и будет «прокидывать» контекст вниз для всех своих children
const Theme = (props: < children: React.ReactNode >) => < const [theme, setTheme] = useState(getTheme); return ( > > ); >;
И не забудем написать кусочек кода, который сохраняет нам значение в нашем хранилище (localStorage)
const [theme, setTheme] = useState(getTheme); useEffect(() => < localStorage.setItem(StorageKey, theme); document.documentElement.setAttribute('data-theme', theme); >, [theme]);
Вроде бы всё работает, осталось собрать всё воедино.
Однако перед этим давайте попробуем упростить себе жизнь в будущем и сделаем стандартный контрол, который будет изменять значение темы. Назовём его SimpleToggler.
function SimpleToggler() < const < theme, setTheme >= useTheme(); const handleSwitchTheme = () => < if (theme === 'dark') < setTheme('light'); >else < setTheme('dark'); >>; return ( onClick=> data-theme= /> ); > :root < --toggler-padding: 3px; --toggler-border: 2px; --ball-diameter: 14px; --toggler-width: 47px; >.simpleToggler < display: flex; width: var(--toggler-width); border-radius: var(--toggler-width); padding: var(--toggler-padding); background-color: rgb(var(--background-primary)); border: var(--toggler-border) solid rgb(var(--accent)); display: flex; align-items: center; justify-content: space-between; box-sizing: border-box; position: relative; cursor: pointer; transition: backgroundColor 0.2s ease; >.ball < position: relative; z-index: 1; width: var(--ball-diameter); height: var(--ball-diameter); background-color: rgb(var(--background-primary)); background-position: center; background-size: cover; border-radius: 50%; transition: transform 0.2s linear, backgroundColor 0.2s ease; >.ball[data-theme='dark'] < background-image: url('./images/moon.png'); >.ball[data-theme='light'] < background-image: url('./images/sun.png'); >html[data-theme='light'] .simpleToggler < transform: translateX(0); >html[data-theme='dark'] .ball
И для того, чтобы не «размазывать» логику по разным файлам мы применим подход Compound components.
Итак, собираем всё вместе:
import React, < useEffect, createContext, useState, useContext >from 'react'; import Styles from './index.module.css'; const StorageKey = 'features-color-theme'; const supportedThemes = < light: 'light', dark: 'dark', >; type Themes = keyof typeof supportedThemes; const ThemeContext = createContext < | < theme: Themes; setTheme: (theme: Themes) =>void; supportedThemes: < Data theme in css: string >; > | undefined >(undefined); const useTheme = () => < const context = useContext(ThemeContext); if (!context) < throw new Error( 'You can use "useTheme" hook only within a component.' ); > return context; >; const getTheme = (): Themes => < let theme = localStorage.getItem(StorageKey); if (!theme) < localStorage.setItem(StorageKey, 'light'); theme = 'light'; >return theme as Themes; >; const Theme = (props: < children: React.ReactNode >) => < const [theme, setTheme] = useState(getTheme); useEffect(() => < localStorage.setItem(StorageKey, theme); document.documentElement.setAttribute('data-theme', theme); >, [theme]); return ( > > ); >; Theme.SimpleToggler = function SimpleToggler() < const < theme, setTheme >= useTheme(); const handleSwitchTheme = () => < if (theme === 'dark') < setTheme('light'); >else < setTheme('dark'); >>; return ( onClick=> data-theme= /> ); >; export < useTheme >; export default Theme; Применяем то, что получилось
Нам нужно обернуть в наш провайдер главный компонент приложения:
import React from 'react'; import ReactDOM from 'react-dom/client'; import Theme from './components/theme-provider'; import App from './pages'; import './index.css'; import './components/theme-provider/themes.css'; const root = document.getElementById('root') as HTMLElement; ReactDOM.createRoot(root).render( );
А в самих компонентах можем уже использовать то, что нам больше нравится и/или нужно:
import Theme from './components/theme-provider'; import Styles from './index.module.css'; const Component = () => < return ( > ); >;
import < useTheme >from './components/theme-provider'; import Styles from './index.module.css'; const option_1 = '1'; const option_2 = '2'; const Component = () => < const < theme >= useTheme(); const value = theme === 'dark' ? option_1 : option_2; return ( >* some markup that use selected theme value */> ); >;
Итого
Получили в итоге гибкую систему, которая отвечает всем изначально поставленным требованиям:
- Имеем возможность расширения
- описываем токены, собираем из них тему, используем
- css переменные делают цвета темы доступной;
- кастомный хук позволяет настраивать внешние компоненты по нашим потребностям
- используем контекст для этого
Summary по решениям:
- триады для описание цвета
- [data-theme=’light’] слектор по аттрибуту и custom properties для применения в css.
Применение и изменение темы в react приложения.
- localStorage для сохранения значения темы
- createContext react контекст, для получения доступа к теме и её изменению на любом уровне вложенности
- useTheme кастомный хук, для упрощения доступа к контексту
Спасибо за чтение и удачи в темизации ваших приложений)