← HTML & CSS/CSS-in-JS: styled-components и Emotion#40 из 383← ПредыдущийСледующий →+20 XP
Полезно по теме:Гайд: старт в frontendПрактика: DOM и событияТермин: DOMМаршрут: старт с нуля

CSS-in-JS: styled-components и Emotion

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

Концепция

Проблема обычного CSS в масштабе:

  • Глобальные имена классов конфликтуют
  • Мёртвый код сложно отследить — неизвестно, используется ли класс
  • Динамические стили требуют манипуляций с классами
  • CSS-in-JS решает это: стили привязаны к компоненту, неиспользуемые стили удаляются автоматически.

    styled-components

    import styled from 'styled-components'
    
    // Создаём styled-компонент — возвращает React-компонент
    const Button = styled.button`
      background: ${props => props.primary ? '#7b2ff7' : 'white'};
      color: ${props => props.primary ? 'white' : '#7b2ff7'};
      border: 2px solid #7b2ff7;
      padding: 8px 16px;
      border-radius: 6px;
      cursor: pointer;
    
      &:hover {
        opacity: 0.9;
        transform: translateY(-1px);
      }
    `
    
    // Использование
    <Button>Обычная кнопка</Button>
    <Button primary>Основная кнопка</Button>

    Под капотом: генерирует уникальный хеш класса, инжектирует <style> в <head>.

    Emotion

    import { css, cx } from '@emotion/css'
    
    const buttonStyle = css`
      background: #7b2ff7;
      color: white;
      padding: 8px 16px;
    `
    
    // jsx-метод
    import { jsx } from '@emotion/react'
    const button = <button css={{ background: '#7b2ff7', padding: '8px 16px' }}>Click</button>

    Emotion легче styled-components, поддерживает object syntax и string syntax.

    CSS Modules — компромисс

    /* Button.module.css */
    .button { background: #7b2ff7; color: white; }
    .button--primary { font-weight: bold; }
    import styles from './Button.module.css'
    
    <button className={styles.button}>Click</button>
    // Рендерится как: <button class="Button_button__xK2sA">

    CSS Modules: локальные имена через хеш, но стили остаются в CSS-файлах. Нулевой runtime.

    Runtime vs Zero-runtime

    | Подход | Runtime | Bundle size | SSR | DX |

    |----------------------|---------|-------------|-----|------|

    | styled-components | Да | +30KB | Да | ⭐⭐⭐ |

    | Emotion | Да | +12KB | Да | ⭐⭐⭐ |

    | CSS Modules | Нет | +0KB | Да | ⭐⭐ |

    | Vanilla Extract | Нет | +0KB | Да | ⭐⭐⭐ |

    | Tailwind | Нет | +0KB | Да | ⭐⭐⭐ |

    Zero-runtime (Vanilla Extract, Linaria): компилируются в статический CSS во время сборки.

    Critical CSS

    CSS-in-JS автоматически решает проблему критического CSS — стили добавляются только для компонентов, которые есть на странице:

    // На сервере styled-components собирает только нужные стили
    import { ServerStyleSheet } from 'styled-components'
    
    const sheet = new ServerStyleSheet()
    const html = renderToString(sheet.collectStyles(<App />))
    const styleTags = sheet.getStyleTags()
    // → <style data-styled="...">только использованные стили</style>

    Современный подход: css() функция

    // Emotion / Vanilla Extract
    const styles = css({
      display: 'flex',
      gap: '16px',
      '@media (max-width: 768px)': {
        flexDirection: 'column',
      },
      ':hover': {
        opacity: 0.8,
      },
    })

    Примеры

    Минимальная реализация CSS-in-JS: тегированный шаблонный литерал, генерация уникальных классов и инъекция стилей

    // Мини CSS-in-JS система — как работает styled-components внутри
    let classCounter = 0
    const injectedStyles = new Map()
    
    function generateClassName() {
      return `sc-${(++classCounter).toString(36)}`
    }
    
    function injectStyle(className, cssText) {
      if (!injectedStyles.has(className)) {
        injectedStyles.set(className, cssText)
    
        // В браузере: инжектируем в <style>
        let styleTag = document.getElementById('css-in-js-styles')
        if (!styleTag) {
          styleTag = document.createElement('style')
          styleTag.id = 'css-in-js-styles'
          document.head.appendChild(styleTag)
        }
        styleTag.textContent += `.${className} { ${cssText} }\n`
      }
    }
    
    // Tagged template literal функция
    function css(strings, ...values) {
      const cssText = strings.reduce((acc, str, i) => {
        return acc + str + (values[i] !== undefined ? values[i] : '')
      }, '').trim()
    
      const className = generateClassName()
      injectStyle(className, cssText)
      return className
    }
    
    // Создаём "компоненты" через factory
    function createStyledEl(tag) {
      return function(strings, ...values) {
        const cssText = strings.reduce((acc, str, i) => {
          return acc + str + (values[i] !== undefined ? values[i] : '')
        }, '').trim()
    
        return function(props = {}) {
          // Вычисляем финальный CSS с учётом props (упрощённо)
          const el = document.createElement(tag)
          const className = generateClassName()
          injectStyle(className, cssText)
          el.className = className
          if (props.textContent) el.textContent = props.textContent
          return el
        }
      }
    }
    
    const styled = {
      div: createStyledEl('div'),
      button: createStyledEl('button'),
      span: createStyledEl('span'),
    }
    
    // Используем нашу мини-систему
    const Card = styled.div`
      background: white;
      border-radius: 8px;
      padding: 16px;
      box-shadow: 0 2px 8px rgba(0,0,0,0.1);
      font-family: sans-serif;
      margin: 8px;
    `
    
    const PrimaryButton = styled.button`
      background: #7b2ff7;
      color: white;
      border: none;
      padding: 8px 16px;
      border-radius: 6px;
      cursor: pointer;
    `
    
    // Создаём элементы
    const card = Card({ textContent: '' })
    document.body.appendChild(card)
    
    const title = document.createElement('h3')
    title.textContent = 'CSS-in-JS компонент'
    title.style.margin = '0 0 8px'
    card.appendChild(title)
    
    const btn = PrimaryButton({ textContent: 'Кнопка' })
    card.appendChild(btn)
    
    // Выводим результат
    console.log('Инжектированные классы:', [...injectedStyles.keys()])
    console.log('Количество инжектированных стилей:', injectedStyles.size)
    console.log('Card className:', card.className)
    console.log('Button className:', btn.className)
    
    // Проверяем что стили инжектированы
    const styleTag = document.getElementById('css-in-js-styles')
    console.log('Style tag содержит правил:', styleTag.textContent.split('\n').filter(l => l.trim()).length)

    CSS-in-JS: styled-components и Emotion

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

    Концепция

    Проблема обычного CSS в масштабе:

  • Глобальные имена классов конфликтуют
  • Мёртвый код сложно отследить — неизвестно, используется ли класс
  • Динамические стили требуют манипуляций с классами
  • CSS-in-JS решает это: стили привязаны к компоненту, неиспользуемые стили удаляются автоматически.

    styled-components

    import styled from 'styled-components'
    
    // Создаём styled-компонент — возвращает React-компонент
    const Button = styled.button`
      background: ${props => props.primary ? '#7b2ff7' : 'white'};
      color: ${props => props.primary ? 'white' : '#7b2ff7'};
      border: 2px solid #7b2ff7;
      padding: 8px 16px;
      border-radius: 6px;
      cursor: pointer;
    
      &:hover {
        opacity: 0.9;
        transform: translateY(-1px);
      }
    `
    
    // Использование
    <Button>Обычная кнопка</Button>
    <Button primary>Основная кнопка</Button>

    Под капотом: генерирует уникальный хеш класса, инжектирует <style> в <head>.

    Emotion

    import { css, cx } from '@emotion/css'
    
    const buttonStyle = css`
      background: #7b2ff7;
      color: white;
      padding: 8px 16px;
    `
    
    // jsx-метод
    import { jsx } from '@emotion/react'
    const button = <button css={{ background: '#7b2ff7', padding: '8px 16px' }}>Click</button>

    Emotion легче styled-components, поддерживает object syntax и string syntax.

    CSS Modules — компромисс

    /* Button.module.css */
    .button { background: #7b2ff7; color: white; }
    .button--primary { font-weight: bold; }
    import styles from './Button.module.css'
    
    <button className={styles.button}>Click</button>
    // Рендерится как: <button class="Button_button__xK2sA">

    CSS Modules: локальные имена через хеш, но стили остаются в CSS-файлах. Нулевой runtime.

    Runtime vs Zero-runtime

    | Подход | Runtime | Bundle size | SSR | DX |

    |----------------------|---------|-------------|-----|------|

    | styled-components | Да | +30KB | Да | ⭐⭐⭐ |

    | Emotion | Да | +12KB | Да | ⭐⭐⭐ |

    | CSS Modules | Нет | +0KB | Да | ⭐⭐ |

    | Vanilla Extract | Нет | +0KB | Да | ⭐⭐⭐ |

    | Tailwind | Нет | +0KB | Да | ⭐⭐⭐ |

    Zero-runtime (Vanilla Extract, Linaria): компилируются в статический CSS во время сборки.

    Critical CSS

    CSS-in-JS автоматически решает проблему критического CSS — стили добавляются только для компонентов, которые есть на странице:

    // На сервере styled-components собирает только нужные стили
    import { ServerStyleSheet } from 'styled-components'
    
    const sheet = new ServerStyleSheet()
    const html = renderToString(sheet.collectStyles(<App />))
    const styleTags = sheet.getStyleTags()
    // → <style data-styled="...">только использованные стили</style>

    Современный подход: css() функция

    // Emotion / Vanilla Extract
    const styles = css({
      display: 'flex',
      gap: '16px',
      '@media (max-width: 768px)': {
        flexDirection: 'column',
      },
      ':hover': {
        opacity: 0.8,
      },
    })

    Примеры

    Минимальная реализация CSS-in-JS: тегированный шаблонный литерал, генерация уникальных классов и инъекция стилей

    // Мини CSS-in-JS система — как работает styled-components внутри
    let classCounter = 0
    const injectedStyles = new Map()
    
    function generateClassName() {
      return `sc-${(++classCounter).toString(36)}`
    }
    
    function injectStyle(className, cssText) {
      if (!injectedStyles.has(className)) {
        injectedStyles.set(className, cssText)
    
        // В браузере: инжектируем в <style>
        let styleTag = document.getElementById('css-in-js-styles')
        if (!styleTag) {
          styleTag = document.createElement('style')
          styleTag.id = 'css-in-js-styles'
          document.head.appendChild(styleTag)
        }
        styleTag.textContent += `.${className} { ${cssText} }\n`
      }
    }
    
    // Tagged template literal функция
    function css(strings, ...values) {
      const cssText = strings.reduce((acc, str, i) => {
        return acc + str + (values[i] !== undefined ? values[i] : '')
      }, '').trim()
    
      const className = generateClassName()
      injectStyle(className, cssText)
      return className
    }
    
    // Создаём "компоненты" через factory
    function createStyledEl(tag) {
      return function(strings, ...values) {
        const cssText = strings.reduce((acc, str, i) => {
          return acc + str + (values[i] !== undefined ? values[i] : '')
        }, '').trim()
    
        return function(props = {}) {
          // Вычисляем финальный CSS с учётом props (упрощённо)
          const el = document.createElement(tag)
          const className = generateClassName()
          injectStyle(className, cssText)
          el.className = className
          if (props.textContent) el.textContent = props.textContent
          return el
        }
      }
    }
    
    const styled = {
      div: createStyledEl('div'),
      button: createStyledEl('button'),
      span: createStyledEl('span'),
    }
    
    // Используем нашу мини-систему
    const Card = styled.div`
      background: white;
      border-radius: 8px;
      padding: 16px;
      box-shadow: 0 2px 8px rgba(0,0,0,0.1);
      font-family: sans-serif;
      margin: 8px;
    `
    
    const PrimaryButton = styled.button`
      background: #7b2ff7;
      color: white;
      border: none;
      padding: 8px 16px;
      border-radius: 6px;
      cursor: pointer;
    `
    
    // Создаём элементы
    const card = Card({ textContent: '' })
    document.body.appendChild(card)
    
    const title = document.createElement('h3')
    title.textContent = 'CSS-in-JS компонент'
    title.style.margin = '0 0 8px'
    card.appendChild(title)
    
    const btn = PrimaryButton({ textContent: 'Кнопка' })
    card.appendChild(btn)
    
    // Выводим результат
    console.log('Инжектированные классы:', [...injectedStyles.keys()])
    console.log('Количество инжектированных стилей:', injectedStyles.size)
    console.log('Card className:', card.className)
    console.log('Button className:', btn.className)
    
    // Проверяем что стили инжектированы
    const styleTag = document.getElementById('css-in-js-styles')
    console.log('Style tag содержит правил:', styleTag.textContent.split('\n').filter(l => l.trim()).length)

    Задание

    CSS-in-JS — это когда стили пишутся в JS, но результат — обычный CSS. Напиши итоговый HTML с CSS-стилями, которые имитируют то, что сгенерировал бы styled-components. Компонент `Card` должен иметь белый фон, тень и скруглённые углы. Компонент `Button` с пропсом `primary` — фиолетовый фон, без `primary` — прозрачный с фиолетовой рамкой.

    Подсказка

    Card: `background: white`, `border-radius: 12px`, `padding: 20px`, `box-shadow: 0 2px 8px rgba(0,0,0,0.1)`. Button primary: `background: #7b2ff7`, `color: white`. Button outline: `background: transparent`, `color: #7b2ff7`. Hover: `opacity: 0.85`.

    Загружаем среду выполнения...
    Загружаем AI-помощника...