Передача данных в CSS

При разработке интерфейсов часто возникает необходимость динамически менять стили HTML-элементов в зависимости от текущего состояния приложения. Существует довольно много способов передать данные из Javascript в CSS и в этой статье я предлагаю их рассмотреть.
Инлайн-стили
Самый простой способ стилизовать элемент — указать CSS-свойства прямо в HTML:
<p style="color: #272727;">Lorem ipsum dolor sit.</p>
Этот древнейший споособ стилизации максимально прост в использовании, но имеет ряд существенных недостатков:
-
Смешивается структура страницы и её оформление (мы же не пишем Javascript прямо в HTML);
-
Нет возможности использовать каскад, медиа-выражения и некоторые другие инструменты, доступные в CSS;
-
Нет возможности переиспользовать стили для одинаковых элементов.
В JSX последний пункт можно решить с помощью создания переменной:
const paragraphStyles = { color: "#272727" };
const MyComponent = () => ( <div> <p style={paragraphStyles}>Lorem ipsum dolor sit amet</p> <p style={paragraphStyles}>Lorem ipsum dolor sit amet</p> </div>);
Похожий подход применяется в
CSS-классы
Проблема выше решается путём выноса стилей из HTML:
<style> .my-paragraph { color: #272727; }</style>
<div> <p class="my-paragraph">Lorem ipsum dolor sit amet</p> <p class="my-paragraph">Lorem ipsum dolor sit amet</p></div>
Придумывая название класса, мы создаём некую абстракцию, которую зачастую наделяем смыслом. Вероятно этот класс будет представлять собой некий компонент, но у компонентов в привычном нам понимании бывают свойства, которые можно менять при необходиости. И это не CSS свойства — мы создали абстракцию в виде класса над стилями, не будем спешить её ломать, смешивая её с конкретными стилевыми параметрами.
CSS in JS
Раз речь зашла о компонентах, попробуем вернуться к React и посмотреть, как подобная задача решается там.
В общем-то очень остроумно проблему решают CSS-in-JS фреймворки: чтобы не передавать данные из JS в CSS, мы перенесли CSS в JS:
import styled from "styled-components";
const Paragraph = styled.p` color: #272727;`;
const MyComponent = () => ( <div> <Paragraph>Lorem ipsum dolor sit amet</Paragraph> <Paragraph>Lorem ipsum dolor sit amet</Paragraph> </div>);
Если нам понадобится передать данные из JS в CSS, мы можем сделать это так же, как и с любым другим React-компонентом:
import styled, { css } from "styled-components";
interface ParagraphProps { type?: "regular" | "important";}
const paragraphTypeStyles = { regular: css` color: #272727; `, important: css` background-color: #ff6287; font-weight: bold; `,};
const Paragraph = styled.p<ParagraphProps>` ${({ type = "regular" }) => paragraphTypeStyles[type]}`;
const MyComponent = () => ( <div> <Paragraph>Lorem ipsum dolor sit amet</Paragraph> <Paragraph type="important">Lorem ipsum dolor sit amet</Paragraph> </div>);
Мы передаём некий дискретный набор данных в компонент и для каждой новой их комбинации
import { useEffect, useState } from "react";import styled from "styled-components";
interface BallProps { position: [x: number, y: number];}
const Ball = styled.div<BallProps>` width: 32px; height: 32px; border-radius: 16px; background-color: #ff6287; transform: ${({ position }) => `translate(${position[0]}px, ${position[1]}px)`};`;
const MyComponent = () => { const [position, setPosition] = useState([0, 0]);
useEffect(() => { let rafId;
/** Меняем позицию 60 раз в секунду */ const update = (time: number) => { setPosition([Math.sin(time * 30), Math.cos(time * 30)]);
rafId = window.requestAnimationFrame(update); };
rafId = window.requestAnimationFrame(update);
return () => { window.cancelAnimationFrame(rawId); }; }, []);
return <Ball position={position} />;};
Спустя некоторое время styled-components вежливо намекнёт, что так делать не стоит, прямо в консоли браузера:
Для этого случая нам предлагают использовать инлайновые стили, но по прежнему они спрятаны за абстракцией в виде пропсы position
, за счёт чего стили отделены от логики:
import { useEffect, useState } from "react";import styled from "styled-components";
interface BallProps { position: [x: number, y: number];}
const Ball = styled.div<BallProps>.attrs(({ position }: BallProps) => ({ style: { transform: `translate(${position[0]}px, ${position[1]}px)` }}))` width: 32px; height: 32px; border-radius: 16px; background-color: #ff6287;`;
const MyComponent = () => { const [position, setPosition] = useState([0, 0]);
// ...
return <Ball position={position} />;};
Но что если СSS-in-JS решение не подходит?
Решаем задачу нативно
-
булевыми
<input class="input input_invalid" /><style>.input__invalid {border-color: red;}</style> -
ключ-значение
<p class="paragraph paragraph_type_important">Lorem ipsum</p>
Аналогично можно использовать data-атрибуты:
<input class="input" data-invalid="true" />
<style> .input[data-invalid="true"] { border-color: red; }</style>
Во многих случаях, кстати, data-атрибуты можно заменить на
<input class="input" aria-invalid="true" />
<style> .input[aria-invalid="true"] { border-color: red; }</style>
Перечисленные выше решения опять же позволяют создавать конечные наборы значений. Вот бы был способ передать Javascript-переменную в CSS.
Что? Такой способ уже есть?
CSS Custom Properties
С их помощью можно в том числе организовать API между бизнес-логикой и визуалом:
<div class="ball" style="--position-x: 35px; --position-y: 54px;"></div>
<style> .ball { width: 32px; height: 32px; border-radius: 16px; background-color: #ff6287; transform: translate(var(--position-x), var(--position-y)); }</style>
То же самое возможно и в JSX (
const MyComponent = () => { const [position, setPosition] = useState([0, 0]);
// ...
return ( <div className="ball" style={{ "--position-x": position[0], "--position-y": position[1], }} /> );};
И самая главная мощь тут заключается в том, что CSS-переменные действуют каскадно!
То есть мы можем разработать компонент, состоящий из нескольких вложенных друг в друга HTML-элементов, установить значение СSS-переменной на корневом элементе и использовать его в потомках:
<div id="cube-demo" class="viewport" style="--scroll-top: 0"> <div class="cube"> <div class="cube-edge cube-edge_front"></div> <div class="cube-edge cube-edge_left"></div> <div class="cube-edge cube-edge_back"></div> <div class="cube-edge cube-edge_right"></div> <div class="cube-edge cube-edge_top"></div> </div></div>
<style> .cube { transform: rotateY( calc(var(--scroll-top) * 1deg) /* Не забудем о единицах измерения */ ); }</style>
<script> document.addEventListener("scroll", ({ event }) => { document .getElementById("cube-demo") .style.setAttribute("--scroll-top", event.target.scrollTop.toString()); });</script>
При большом желании можно даже сохранять данные в СSS-переменных:
<form id="color-picker-demo" style="--color-red: 255; --color-green: 216; --color-blue: 101;"> <input type="range" name="color-red" /> <input type="range" name="color-green" /> <input type="range" name="color-blue" /> <div class="preview">Hacknote.js</div></form>
<script> const demo = document.getElementById("color-picker-demo");
demo.addEventListener("input", ({ target }) => { demo.style.setProperty(`--${target.name}`, target.value); });</script>
За счёт каскада довольно удобно делать темизацию:
<body data-theme="dark"> <p>Lorem ipsum dolor sit</p></body>
<style> body[data-theme="light"] { --text-color: back; --bg-color: white; }
body[data-theme="dark"] { --text-color: white; --bg-color: black; }</style>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Debitis cumque sit totam. Id repudiandae corporis quas excepturi ab laborum non soluta cumque aut? Minus sit a autem atque eligendi adipisci?
Заключение
На самом деле вся статья задумывалась, чтобы показать, что в 2023 году у СSS-in-JS решений практически не осталось преимуществ перед обычным СSS в отдельном файлике и их использование не всегда оправдано.