Привет, Хабр!

В одном из проектов мне нужно было создать сложную админку для крупного клиента. Интерфейс требовал большо количества интерактивностей, а также поддержку различных тем и динамических стилей. Именно тогда я решил обратить свой взгляд на CSS‑in‑JS библиотеки, и это для меня стало большим открытием.

Ведь стилизация компонентов — основа для создания интуитивно понятных и эстетически приятных интерфейсов. И иногда традиционные методы стилизации, такие как CSS‑файлы или препроцессоры, имеют свои ограничения и могут усложнять сам процесс разработки, чего мы точно не хотим.

Здесь вот и приходят на помощь решения CSS‑in‑JS, объединяющие фичи JS и CSS.

CSS‑in‑JS — это подход к стилизации, который позволяет писать стили прямо в JavaScript‑коде. Преимущества такого подхода:

  1. Изоляция стилей: компоненты получают свои собственные стили.

  2. Динамические стили: легко применять стили в зависимости от состояния компонента или пропсов.

  3. Поддержка тем: удобное управление темами и их переключение на лету.

  4. Интеграция с JavaScript.

В этой статье я хотел бы представить свой ТОП-5 лучших решений CSS‑in‑JS, которые я использовал.

Styled Components

Styled Components — это библиотека для стилизации React-компонентов с использованием ES6 и шаблонных литералов. Она позволяет писать CSS в JavaScript, создавая стилизованные компоненты, которые инкапсулируют свои стили. Так стилизация становится более модульной и управляемой.

Styled Components использует теги шаблонных литералов для написания CSS внутри самого JS.

Для начала установим библиотеку с помощью npm или yarn:

npm install styled-components
# или
yarn add styled-components

После установки можно импортировать библиотеку.

Пример базового использования:

import styled from 'styled-components';

const Button = styled.button`
  background-color: #007bff;
  color: white;
  border: none;
  padding: 10px 20px;
  border-radius: 4px;
  cursor: pointer;

  &:hover {
    background-color: #0056b3;
  }
`;

function App() {
  return (
    <div>
      <h1>Welcome to Styled-Components!</h1>
      <Button>Click me</Button>
    </div>
  );
}

export default App;

Создали компонент Button, который можно использовать как обычный React-компонент с предопределенными стилями. Стили можно изменять на основе пропсов, используя функцию внутри шаблонных литералов:

const Button = styled.button`
  background-color: ${props => props.primary ? '#007bff' : 'white'};
  color: ${props => props.primary ? 'white' : 'black'};
  /* ... другие стили ... */
`;

function App() {
  return (
    <div>
      <Button primary>Primary Button</Button>
      <Button>Secondary Button</Button>
    </div>
  );
}

Styled Components поддерживает темизацию. Для этого можно использовать ThemeProvider:

import { ThemeProvider } from 'styled-components';

const theme = {
  primaryColor: '#007bff',
  secondaryColor: '#6c757d',
};

const Button = styled.button`
  background-color: ${props => props.primary ? props.theme.primaryColor : props.theme.secondaryColor};
  /* ... другие стили ... */
`;

function App() {
  return (
    <ThemeProvider theme={theme}>
      <div>
        <Button primary>Primary Button</Button>
        <Button>Secondary Button</Button>
      </div>
    </ThemeProvider>
  );
}

export default App;

ЗдесьButton использует свойства темы для применения стилей.

Styled Components также позволяет создавать глобальные стили с помощью createGlobalStyle:

import { createGlobalStyle } from 'styled-components';

const GlobalStyle = createGlobalStyle`
  body {
    font-family: 'Arial', sans-serif;
    background-color: #f0f0f0;
  }
`;

function App() {
  return (
    <>
      <GlobalStyle />
      <div>
        <h1>Hello, World!</h1>
      </div>
    </>
  );
}

export default App;

Styled Components — очень мощный инструмент. Если вы еще не пробовали его, настоятельно рекомендую!

А подробнее с библиотекой можно ознакомиться здесь

Linaria

Linaria — это zero-runtime CSS-in-JS библиотека, которая преобразует стили, написанные в JS, в отдельные CSS файлы на этапе сборки. Все это для снижения затрат на выполнение стилей во время работы приложения.

Linaria использует шаблонные литералы для определения стилей, которые затем компилируются в чистые CSS файлы. Она поддерживает фичи современного CSS: переменные, медиа-запросы, интеграцию с популярными инструментами сборки, такими как Webpack и Rollup.

Для начала использования Linaria, установим необходимые пакеты:

npm install @linaria/core @linaria/react @linaria/babel-preset

Затем настроим Babel, добавив @linaria в Babel конфиг:

{
  "presets": [
    "@babel/preset-env",
    "@babel/preset-react",
    "@linaria/babel-preset"
  ]
}

Linaria позволяет создавать стили с помощью функции css и компоненты с помощью styled:

import { css } from '@linaria/core';

const titleStyle = css`
  font-size: 24px;
  color: #333;
  text-align: center;
`;

function Title() {
  return <h1 className={titleStyle}>Hello, Linaria!</h1>;
}

export default Title;

Создали класс titleStyle и применяем его к компоненту Title.

Linaria также поддерживает создание стилизованных компонентов с функцией styled, аналогично Styled Components:

import { styled } from '@linaria/react';

const Button = styled.button`
  background-color: #007bff;
  color: white;
  border: none;
  padding: 10px 20px;
  border-radius: 4px;
  cursor: pointer;

  &:hover {
    background-color: #0056b3;
  }
`;

function App() {
  return (
    <div>
      <h1>Welcome to Linaria!</h1>
      <Button>Click me</Button>
    </div>
  );
}

export default App;

Пример использования Linaria для создания темы в React-приложении:

// theme.js
export const theme = {
  colors: {
    primary: '#007bff',
    secondary: '#6c757d',
  },
};

// Button.js
import { styled } from '@linaria/react';
import { theme } from './theme';

const Button = styled.button`
  background-color: ${theme.colors.primary};
  color: white;
  border: none;
  padding: 10px 20px;
  border-radius: 4px;
  cursor: pointer;

  &:hover {
    background-color: ${theme.colors.secondary};
  }
`;

export default Button;

// App.js
import React from 'react';
import Button from './Button';

function App() {
  return (
    <div>
      <h1>Welcome to Themed Linaria!</h1>
      <Button>Click me</Button>
    </div>
  );
}

export default App;

Создаем тему в отдельном файле и используем ее в стилизованном компоненте Button. Стили кнопки изменяются на основе свойств темы.

Подробнее с Linaria можно ознакомиться здесь.

Emotion

Emotion — это высокопроизводительная библиотека для CSS-in-JS, поддерживающая как стильные компоненты, так и базовые CSS стили.

Emotion предлагает два основных способа написания стилей: через styled-компоненты и через css-утилиту.

Для использования Emotion, установим необходимые пакеты:

npm install @emotion/react @emotion/styled

Создание стилей с помощью css:
css — это утилита, которая позволяет определять стили как объекты или строки.

/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react';

const style = css`
  color: hotpink;
`;

function App() {
  return <div css={style}>Hello, Emotion!</div>;
}

export default App;

Здесь юзаем css для определения стиля и применяем его к элементу через атрибут css.

Создание стилизованных компонентов с помощью styled:
styled позволяет создавать React-компоненты с инкапсулированными стилями.

import styled from '@emotion/styled';

const Button = styled.button`
  background-color: #007bff;
  color: white;
  border: none;
  padding: 10px 20px;
  border-radius: 4px;
  cursor: pointer;

  &:hover {
    background-color: #0056b3;
  }
`;

function App() {
  return (
    <div>
      <Button>Click me</Button>
    </div>
  );
}

export default App;

Динамические стили:
Emotion позволяет легко применять динамические стили на основе пропсов.

const Button = styled.button`
  background-color: ${props => props.primary ? '#007bff' : 'white'};
  color: ${props => props.primary ? 'white' : '#007bff'};
  border: 2px solid #007bff;
  padding: 10px 20px;
  border-radius: 4px;
  cursor: pointer;

  &:hover {
    background-color: ${props => props.primary ? '#0056b3' : '#e0e0e0'};
  }
`;

function App() {
  return (
    <div>
      <Button primary>Primary Button</Button>
      <Button>Secondary Button</Button>
    </div>
  );
}

export default App;

Темизация:
Emotion поддерживает темизацию через контекст и ThemeProvider.

import { ThemeProvider } from '@emotion/react';

const theme = {
  colors: {
    primary: '#007bff',
    secondary: '#6c757d',
  },
};

const Button = styled.button`
  background-color: ${props => props.theme.colors.primary};
  color: white;
  border: none;
  padding: 10px 20px;
  border-radius: 4px;
  cursor: pointer;

  &:hover {
    background-color: ${props => props.theme.colors.secondary};
  }
`;

function App() {
  return (
    <ThemeProvider theme={theme}>
      <div>
        <Button>Click me</Button>
      </div>
    </ThemeProvider>
  );
}

export default App;

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

Пример использования Emotion для создания адаптивного интерфейса:

import styled from '@emotion/styled';

const Container = styled.div`
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 20px;

  @media (min-width: 600px) {
    flex-direction: row;
  }
`;

const Item = styled.div`
  background-color: #f0f0f0;
  margin: 10px;
  padding: 20px;
  border-radius: 4px;
`;

function App() {
  return (
    <Container>
      <Item>Item 1</Item>
      <Item>Item 2</Item>
      <Item>Item 3</Item>
    </Container>
  );
}

export default App;

В этом кодеContainer меняет направление флекс-контейнера в зависимости от ширины экрана.

Документацию к Emotion можно посмотреть здесь

Stitches

Stitches предлагает удобный API, поддержку тем и динамических стилей, а также интеграцию с фреймворками и инструментами сборки

Установим библиотеку через npm или yarn:

npm install @stitches/react
# или
yarn add @stitches/react

Функция createStitches позволяет определить конфигурацию для стилей и создавать styled-компоненты:

import { createStitches } from '@stitches/react';

const { styled, css, globalCss, theme } = createStitches({
  theme: {
    colors: {
      primary: '#007bff',
      secondary: '#6c757d',
    },
    fontSizes: {
      body: '16px',
      heading: '24px',
    },
  },
});

const Button = styled('button', {
  backgroundColor: '$primary',
  color: 'white',
  padding: '10px 20px',
  borderRadius: '4px',
  border: 'none',
  cursor: 'pointer',

  '&:hover': {
    backgroundColor: '$secondary',
  },
});

Создали кнопку с использованием тем и стилей, определенных в конфигурации createStitches.

Stitches позволяет задавать глобальные стили через функцию globalCss:

const globalStyles = globalCss({
  body: {
    margin: 0,
    fontFamily: 'Arial, sans-serif',
  },
});

function App() {
  globalStyles();

  return (
    <div>
      <h1>Hello, Stitches!</h1>
      <Button>Click me</Button>
    </div>
  );
}

export default App;

Stitches позволяет легко применять динамические стили на основе пропсов:

const Button = styled('button', {
  variants: {
    color: {
      primary: {
        backgroundColor: '$primary',
        color: 'white',
      },
      secondary: {
        backgroundColor: '$secondary',
        color: 'white',
      },
    },
  },
  defaultVariants: {
    color: 'primary',
  },
});

function App() {
  return (
    <div>
      <Button color="primary">Primary Button</Button>
      <Button color="secondary">Secondary Button</Button>
    </div>
  );
}

export default App;

Stitches поддерживает создание и использование тем:

const lightTheme = theme({
  colors: {
    background: 'white',
    text: 'black',
  },
});

const darkTheme = theme({
  colors: {
    background: 'black',
    text: 'white',
  },
});

const Container = styled('div', {
  backgroundColor: '$background',
  color: '$text',
  padding: '20px',
  borderRadius: '8px',
});

function App() {
  const [isDark, setIsDark] = React.useState(false);

  return (
    <div className={isDark ? darkTheme : lightTheme}>
      <Container>
        <h1>Hello, Stitches!</h1>
        <Button onClick={() => setIsDark(!isDark)}>
          Toggle Theme
        </Button>
      </Container>
    </div>
  );
}

export default App;

Пример кода Stitches для создания адаптивного интерфейса с динамическими стилями:

import { createStitches } from '@stitches/react';

const { styled, globalCss } = createStitches({
  theme: {
    colors: {
      primary: '#007bff',
      secondary: '#6c757d',
    },
  },
});

const globalStyles = globalCss({
  body: {
    margin: 0,
    fontFamily: 'Arial, sans-serif',
  },
});

const Container = styled('div', {
  display: 'flex',
  flexDirection: 'column',
  alignItems: 'center',
  padding: '20px',

  '@media(min-width: 600px)': {
    flexDirection: 'row',
  },
});

const Item = styled('div', {
  backgroundColor: '#f0f0f0',
  margin: '10px',
  padding: '20px',
  borderRadius: '4px',
});

function App() {
  globalStyles();

  return (
    <Container>
      <Item>Item 1</Item>
      <Item>Item 2</Item>
      <Item>Item 3</Item>
    </Container>
  );
}

export default App;

Здесь Container будет менять направление флекс-контейнера в зависимости от ширины экрана.

Как можно заметить, строчек здесь уже намного больше, чем в аналогичном примере у Emotion.

Подробнее со Stiches можно ознакомиться здесь

Vanilla-Extract

Vanilla-Extract — это zero-runtime CSS-in-JS библиотека, которая позволяет писать стили в TypeScript или JavaScript и компилировать их в статические CSS файлы на этапе сборки.

Vanilla-Extract использует функции для определения стилей и тем, которые затем компилируются в отдельные CSS файлы.

Установим:

npm install @vanilla-extract/css

Затем настроим сборщик, например Webpack, для работы с Vanilla-Extract. Для этого нужно добавить плагин в конфигурацию Webpack.

Пример настройки Webpack:

// webpack.config.js
const { VanillaExtractPlugin } = require('@vanilla-extract/webpack-plugin');

module.exports = {
  // ... другие настройки
  plugins: [
    new VanillaExtractPlugin(),
  ],
  module: {
    rules: [
      {
        test: /\.css\.ts$/,
        use: [
          'vanilla-extract-loader',
          'ts-loader',
        ],
      },
    ],
  },
};

Vanilla-Extract позволяет создавать стили с помощью функции style и тем через функцию createTheme:

// styles.css.ts
import { style } from '@vanilla-extract/css';

export const button = style({
  backgroundColor: 'blue',
  color: 'white',
  padding: '10px 20px',
  borderRadius: '5px',
  border: 'none',
  cursor: 'pointer',
  ':hover': {
    backgroundColor: 'darkblue',
  },
});

Стили, определенные с помощью Vanilla-Extract, можно использовать в компонентах React или других фреймворках:

// Button.tsx
import React from 'react';
import { button } from './styles.css.ts';

const Button: React.FC = () => {
  return <button className={button}>Click me</button>;
};

export default Button;

Пример использования Vanilla-Extract для создания адаптивного интерфейса с динамическими стилями и темами:

// responsive.css.ts
import { style } from '@vanilla-extract/css';

export const container = style({
  display: 'flex',
  flexDirection: 'column',
  alignItems: 'center',
  padding: '20px',

  '@media': {
    '(min-width: 600px)': {
      flexDirection: 'row',
    },
  },
});

export const item = style({
  backgroundColor: '#f0f0f0',
  margin: '10px',
  padding: '20px',
  borderRadius: '5px',
});
// App.tsx
import React from 'react';
import { container, item } from './responsive.css.ts';

const App: React.FC = () => {
  return (
    <div className={container}>
      <div className={item}>Item 1</div>
      <div className={item}>Item 2</div>
      <div className={item}>Item 3</div>
    </div>
  );
};

export default App;

Container меняет направление флекс‑контейнера в зависимости от ширины экрана.

Подробнее — здесь

Финальные слова

Все представленные инструменты в статье хороши по своему и каждый может найти свой по вкусу. Хотелось бы услышать о вашем опыте работы с CSS-in-JS, какие инструменты использовали и какие нюансы могли бы рассказать?


Все актуальные методы и инструменты веб-разработки можно освоить на онлайн-курсах OTUS: в каталоге можно посмотреть список всех программ, а в календаре — записаться на открытые уроки.

Комментарии (11)


  1. nin-jin
    20.07.2024 20:47

    Забавно, что все эти css-in-js ничего не знают про, собственно, Cascading Style Sheets, а единственная либа, которая про это знает, в топ не попала: https://mol.hyoo.ru/#!section=docs/=xwq9q5_f966fg


    1. DarthVictor
      20.07.2024 20:47
      +2

      И styled и linaria прекрасно работают с каскадностью.


      1. nin-jin
        20.07.2024 20:47

        Действительно, каскад там есть, но кривой - по именам блоков, но не элементов, в терминах БЭМ.


        1. DarthVictor
          20.07.2024 20:47
          +1

          Там каскад по имени сгенерированного класса. Примерно как в css-modules.


          1. nin-jin
            20.07.2024 20:47

            Ну вот в каунтере, как стилизовать кнопку инкремента так, чтобы не задеть остальные кнопки на любой глубине вложенности?


            1. DarthVictor
              20.07.2024 20:47

              import styled from 'styled-components'; // or @linaria/react
              
              const Counter = () => {
                ... 
                return <div>
                  ...
                  <button onClick...>Styled button</button>
                </div>
              };
              
              
              const CounterWithStyledButton = styled(Counter)`
                button {
                  ....
                }
              `

              Примерно так


              1. nin-jin
                20.07.2024 20:47

                Это стилизует все кнопки подряд, а не одну конкретную.


                1. DarthVictor
                  20.07.2024 20:47
                  +1

                  Это стилизует кнопки внутри компонента. Если вам нужно одну кнопку компонента, то нужно было так и писать. Будет соответственно

                  import styled from 'styled-components'; // or @linaria/react
                  ...
                  const MyBytton = styled.button`
                    ...
                  `;
                  ...
                  
                  const Counter = () => {
                    ... 
                    return <div>
                      ...
                      <button onClick...>Unstyled button</button>
                      <button onClick...>Unstyled button</button>
                      <MyBytton onClick...>Styled button</MyButton>
                    </div>
                  };
                  
                  
                  const CounterWithStyledButton = styled(Counter)`
                    ${MyBytton} {
                      ....
                    }
                  `


                  1. nin-jin
                    20.07.2024 20:47
                    +1

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


  1. functyon
    20.07.2024 20:47
    +9

    всё то же самое умеют css-modules.
    css в коде - то еще удовольствие.


  1. taujavarob
    20.07.2024 20:47

    Странные библиотеки из прошлого.

    Сейчас это tailwind. Как никак на дворе 2024 год.