Клавиша / esc

Обзор CSS-in-JS

CSS в JavaScript — разберём, как можно описывать компоненты и стили, не создав ни одного CSS-файла.

Время чтения: 9 мин

Кратко

Скопировано

CSS-in-JS — это подход, при котором стили пишутся непосредственно в JavaScript-коде компонентов, а не в отдельных CSS-файлах. Это позволяет использовать возможности JavaScript для динамического управления стилями и обеспечивает изоляцию стилей на уровне компонентов, предотвращая конфликты и облегчая поддержку кода.

Существуют разные решения CSS-in-JS, но их объединяет общий принцип: стили описываются непосредственно в JavaScript-коде. Это может делаться с помощью объектов, шаблонных строк или через вызов специальных функций, которые обрабатывают определения стилей. Независимо от конкретного синтаксиса, эти JavaScript-описания затем преобразуются таким образом, что стили применяются к компонентам через уникальные идентификаторы. Чаще всего это автоматически сгенерированные CSS-классы — например, вместо общего класса .button может быть создан уникальный класс вроде .acz123_button или .Component-button-aXyZ. Такой подход обеспечивает изоляцию стилей на уровне компонентов, предотвращая конфликты, и упрощает управление ими.

Вот несколько популярных CSS-in-JS библиотек: Styled Components, Emotion, Styled JSX и vanilla-extract.

Почему CSS-in-JS?

Скопировано

Главное преимущество использования подхода CSS-in-JS — возможность использовать конструкции JavaScript при описании стилей компонента. В зависимости от проекта, это может помочь описывать динамические стили, которые зависят от условий, а также упростить работу с темизацией.

Краткая история

Скопировано

Бегло рассмотрим историю появления CSS-in-JS решений.

Становление CSS

Скопировано

CSS (Каскадные таблицы стилей) начал развитие в 90-е годы, был отмечен консорциумом W3C и начал стремительно развиваться. CSS стал методом определения стиля веб-документа. Он был мощным, чтобы удовлетворить потребности проектов тех лет, и был относительно прост, чтобы все желающие могли приступить к работе в короткий срок.

Появление реактивных фреймворков и SPA

Скопировано

До 2010-х годов, в основном создавались многостраничные приложения с четким разделением ответственности:

  • HTML — определял разметку страницы,
  • CSS — описывал, как они будут выглядеть,
  • JavaScript — отвечал за интерактивность.

С течением времени развивался не только CSS, но и JavaScript — это приводит к появлению новой экосистемы разработки и современных реактивных фреймворков: Angular (2010), React (2013), Vue.js (2014). Разработка приложений начинает смещаться от многостраничных приложений на разработку одностраничных приложений (SPA), а пользователи стали ожидать от них более динамичного и сложного поведения.

Кристофер Шедо, его доклад о проблемах CSS и разработка первых решений CSS-in-JS

Скопировано

В 2014 году Кристофер Шедо (инженер известной социальной сети) выступает с докладом о 7 проблемах CSS, которые нужно решить для беспроблемной разработки больших динамичных приложений:

  1. Глобальное пространство имён
    Все CSS-селекторы живут в одной глобальной области, из-за чего стили разных компонентов могут конфликтовать или перезаписывать друг друга.

  2. Зависимости
    При большом числе стилей сложно понять, в каком порядке подключать файлы и какие компоненты от чего зависят, что может привести к нежелательным коллизиям.

  3. Удаление неиспользуемого кода
    В больших проектах часто копится CSS, который больше нигде не применяется, и его непросто безопасно убрать, не зная точного использования.

  4. Минификация
    При сжатии стилей важно сохранять их читабельность для отладки, но и уменьшать размер, чтобы повысить производительность.

  5. Общий доступ к константам
    Трудно централизованно хранить и переиспользовать значения (например, цвета, размеры), чтобы все команды работали с едиными стилевыми параметрами.

  6. Проблемы каскада
    Когда несколько правил стилизуют одни и те же элементы, порядок подключения CSS может приводить к непредсказуемым результатам и конфликтам.

  7. Изоляция
    Нужен способ гарантировать, что стили каждого компонента не затронут стили других, что особенно актуально в больших проектах.

В этом же году начинается работа над CSS-in-JS. Первыми экспериментальными подходами можно считать решения, появившиеся в экосистеме React. Одна из самых ранних и широко известных библиотек — JSS. Чуть позже начали появляться и другие проекты, но именно JSS часто упоминают как одну из первых полноценных реализаций «CSS в JavaScript».

Runtime и Zero-runtime

Скопировано

За время развития CSS-in-JS появилось множество вариантов реализации CSS-in-JS. От тех, что предполагают полную обработку и генерацию стилей непосредственно в браузере пользователя («Runtime»), до решений, которые практически всю эту работу переносят на этап сборки проекта («Zero-runtime»). Ниже мы рассмотрим ключевые особенности этих двух основных направлений и их специфику.

Runtime подход

Скопировано

Суть подхода

Скопировано

Стили формируются и добавляются в DOM во время исполнения (рантайма) приложения. В процессе рендера или при изменении состояний создаются (или обновляются) соответствующие <style> или классы в браузере.

Плюсы

Скопировано
  • Легко менять стили на лету в зависимости от состояния, темизации и так далее.
  • Удобно изолировать логику стилизации в самих компонентах, сохраняя стиль и логику рядом.

Минусы

Скопировано
  • Дополнительная нагрузка в рантайме: при каждом рендере может происходить генерация и монтирование новых стилей, что влияет на производительность.
  • Возможны сложности и повышенные требования к настройке серверного рендеринга (SSR) и кеширования стилей.

Библиотеки, реализующие подход

Скопировано
  • Styled Components
    Использует шаблонные литералы для описания стилей внутри JavaScript/TypeScript. Генерирует уникальные классы в рантайме и монтирует их в DOM.
  • Emotion
    Также генерирует стили во время исполнения. Позволяет писать стили через шаблонные литералы (styled) или объектный синтаксис (css).
  • JSS
    Основан на использовании JavaScript-объектов, которые конвертируются в CSS в рантайме.

Zero-runtime подход

Скопировано

Суть подхода

Скопировано

Стили вычисляются и извлекаются на этапе сборки. В результате в браузер попадает статический (или почти статический) CSS, а логика генерации стилей не исполняется или почти не исполняется.

Плюсы

Скопировано
  • Нет накладных расходов в браузере (минимальный дополнительный JS-код).
  • Благодаря пункту выше обычно получаются улучшенные показатели производительности и метрик загрузки.
  • Проще реализовать серверный рендеринг, так как стили уже готовы к моменту, когда HTML отдается клиенту.

Минусы

Скопировано
  • Ограничения в динамических стилях: большая часть логики должна быть статически рассчитана во время сборки, поэтому зависимые от состояний стили, могут быть менее гибкими.
  • Сложность настройки сборки: обычно требует дополнительной настройки сборщиков (Webpack, Vite и другие).

Библиотеки, реализующие подход

Скопировано
  • Linaria
    Анализирует CSS, написанный в виде шаблонных литералов, на этапе сборки и извлекает его в отдельные файлы. В рантайме остаются только классы.
  • Vanilla Extract
    Пишется на TypeScript/JS, но генерирует классические CSS-файлы во время сборки, практически без JS для стилизации на клиенте.

Основы синтаксиса

Скопировано

Синтаксис у большинства решений очень похож. Рассмотрим основы синтаксиса на псевдокоде. За подробностями лучше заглянуть в документацию к конкретной библиотеке.

Простейший компонент

Скопировано

Ниже показан пример объявления компонента-кнопки, стили для которого описаны прямо внутри JavaScript-кода. Благодаря функции styled('button') создаётся новый компонент, наследующий все свойства обычной кнопки, но обладающий заданными стилями.

        
          
          import {styled} from "css-in-js" // псевдобиблиотека для примераconst Button = styled('button')`  color: white;  background-color: black;  border: none;`export default Button;
          import {styled} from "css-in-js" // псевдобиблиотека для примера

const Button = styled('button')`
  color: white;
  background-color: black;
  border: none;
`

export default Button;

        
        
          
        
      

Динамические стили

Скопировано

Ниже показана функция, которая генерирует динамические стили для фона зависимости от флага isPrimary. При true функция вернёт синий цвет фона, при false — серый.

        
          
          import {css} from "css-in-js" // псевдобиблиотека для примераfunction getBackgroundColor(isPrimary) {  return css`    background-color: ${isPrimary ? 'blue' : 'gray'};    &:hover {      background-color: ${isPrimary ? 'darkblue' : 'darkgray'};    }  `;}export {getBackgroundColor};
          import {css} from "css-in-js" // псевдобиблиотека для примера

function getBackgroundColor(isPrimary) {
  return css`
    background-color: ${isPrimary ? 'blue' : 'gray'};

    &:hover {
      background-color: ${isPrimary ? 'darkblue' : 'darkgray'};
    }
  `;
}

export {getBackgroundColor};

        
        
          
        
      

В первом фрагменте создаётся функция getBackgroundColor(isPrimary), которая возвращает блок CSS с разными цветами фона в покое и при наведении курсора в зависимости от аргумента isPrimary.

Во втором примере показано, как этот фрагмент может переиспользоваться: в новом компоненте Button вызывается getBackgroundColor(true) и результат вставляется в шаблон стилей, «включая» логику динамической стилизации, уже описанной в функции. Это удобно, если нужно единообразно управлять цветами фона в разных компонентах, сохранив их стили в одном месте.

        
          
          import {styled} from "css-in-js" // псевдобиблиотека для примераimport {getBackgroundColor} from "./utils.js"const Button = styled('button')`  color: white;  border: none;  ${getBackgroundColor(true)};`export default Button;
          import {styled} from "css-in-js" // псевдобиблиотека для примера
import {getBackgroundColor} from "./utils.js"

const Button = styled('button')`
  color: white;
  border: none;
  ${getBackgroundColor(true)};
`

export default Button;

        
        
          
        
      

На практике

Скопировано

Руслан Шевченко советует

Скопировано

🛠️ Какую библиотеку выбрать? Чаще всего выбор подхода к CSS-in-JS определяется требованиями к производительности, объёмом динамики в интерфейсе и готовностью настраивать сборку. При этом часто всё сводится к двум простым сценариям:

  • Если нужна «минимальная боль» при использовании динамических стилей и важна поддержка большого сообщества — часто берут Emotion или Styled Components.
  • Если же приоритет — производительность и нет потребности в постоянном рендере уникальных стилей, можно обратить внимание на Linaria или Vanilla Extract.