В студии PSI мы всегда ищем оптимальный баланс между современными инструментами и производительностью. Когда мы впервые попробовали библиотеки CSS-in-JS вроде Styled Components и Emotion, нас привлекла идея передачи значений и состояния напрямую в стили. Это замыкало цикл с концепцией React, где UI — это функция состояния.
Но у этого подхода были свои проблемы. Сегодня мы покажем, как решать те же задачи с помощью чистого CSS.
Типы динамических стилей
Мы выделяем два основных типа динамической стилизации в React-компонентах:
- Значения: цвет, задержка, позиция — любое единичное значение для CSS-свойства
- Состояния: варианты кнопки, состояние загрузки — каждое со своим набором стилей
Традиционные подходы
Для сравнения мы рассмотрим SCSS (с BEM) и Styled Components — самые популярные способы стилизации в React. Мы не рассматриваем библиотеки, где CSS пишется как JavaScript-объекты — для этого есть отличные решения вроде Vanilla Extract. Наше решение для тех, кто любит писать CSS как CSS, но хочет лучшей интеграции с реактивностью компонентов.
Если вы знакомы с проблемой, переходите к решению.
Проблема с динамическими значениями
Традиционно в чистом CSS динамические значения передавались через встроенные стили:
function Button({ color, children }) {
return (
<button className="button" style={{ backgroundColor: color }}>
{children}
</button>
);
}
Проблемы этого подхода:
- Высокая специфичность — сложно переопределить
- Стили разбросаны между CSS и JSX
- Нет единого источника стилей
CSS-in-JS решал это через props:
function Button({ color, children }) {
return <StyledButton color={color}>{children}</StyledButton>;
}
const StyledButton = styled.button`
border: 0;
border-radius: 4px;
padding: 8px 12px;
font-size: 14px;
color: dimgrey;
background-color: ${props => props.color};
`;
Проблема с состояниями
В традиционном CSS использовалась конкатенация строк классов. С BEM это выглядело так:
function Button({ color, size, primary, children }) {
return (
<button
className={['button', `button--${size}`, primary ? 'button--primary' : null]
.filter(Boolean)
.join(' ')}
style={{ backgroundColor: color }}
>
{children}
</button>
);
}
.button {
border: 0;
border-radius: 4px;
padding: 8px 12px;
font-size: 14px;
color: dimgrey;
background-color: whitesmoke;
&--primary {
background-color: $primary-color;
}
&--small { height: 30px; }
&--medium { height: 40px; }
&--large { height: 60px; }
}
SCSS выглядит чисто, но JSX превращается в кошмар с конкатенацией строк и тернарными операторами.
Styled Components тоже громоздкий:
const StyledButton = styled.button`
border: 0;
border-radius: 4px;
padding: 8px 12px;
font-size: 14px;
color: dimgrey;
background-color: whitesmoke;
${props => props.primary && css`
background-color: $primary-color;
`}
${props => props.size === 'small' && css`
height: 30px;
`}
${props => props.size === 'medium' && css`
height: 40px;
`}
${props => props.size === 'large' && css`
height: 60px;
`}
`;
Повторяющиеся функции для получения props делают код шумным и трудночитаемым.
Наш подход: чистый CSS
В студии PSI мы вернулись к чи стому CSS с пользовательскими свойствами (CSS-переменными). Когда эти методы появились, поддержка браузеров была слабой. Сегодня она практически универсальна.
Мы наблюдаем тренд к платформенным стандартам — фреймворки вроде Remix и Deno придерживаются веб-стандартов, а не изобретают свои решения. То же происходит с CSS: нативные функции становятся лучше, чем препроцессоры и CSS-in-JS.
Вот как мы стилизуем React-компоненты. Технически мы используем postcss для поддержки будущих функций (вложенность, пользовательские медиа-запросы), но по мере роста поддержки браузерами эти инструменты постепенно исчезают.
Динамические значения
Элегантное решение — пользовательские свойства CSS в атрибуте style:
function Button({ color, children }) {
return (
<button className="button" style={{ '--color': color }}>
{children}
</button>
);
}
.button {
border: 0;
border-radius: 4px;
padding: 8px 12px;
font-size: 14px;
color: dimgrey;
background-color: var(--color);
}
Преимущества:
- Нет проблем со специфичностью — свойство объявлено в CSS
- Все стили в одном месте
- Чистый и читаемый код
Это особенно удобно для сложных свойств вроде transform:
// Передаём только нужное значение, а не весь transform
function Button({ offset, children }) {
return (
<button className="button" style={{ '--offset': `${offset}px` }}>
{children}
</button>
);
}
.button {
border: 0;
padding: 8px 12px;
font-size: 14px;
color: dimgrey;
background-color: whitesmoke;
transform: translate3d(0, var(--offset), 0);
}
Состояния компонентов
Для состояний мы используем data-атрибуты. Это прекрасно сочетается с нативным синтаксисом вложенности CSS:
function Button({ color, size, primary, children }) {
return (
<button className="button" data-size={size} data-primary={primary}>
{children}
</button>
);
}
.button {
border: 0;
border-radius: 4px;
padding: 8px 12px;
font-size: 14px;
color: dimgrey;
background-color: whitesmoke;
&[data-primary='true'] {
background-color: var(--colorPrimary);
}
&[data-size='small'] { height: 30px; }
&[data-size='medium'] { height: 40px; }
&[data-size='large'] { height: 60px; }
}
Попробуйте живой пример:
Почему это лучше:
- Похоже на BEM, но без конкатенации строк
- Гораздо читабельнее, чем функции Styled Components
- Единственный компромисс — дополнительная специфичность атрибута
Сначала data-атрибуты для стилей могут показаться непривычными, но они решают проблему грязной конкатенации классов. Плюс это согласуется с тем, как мы стилизуем интерактивные состояния через aria-атрибуты:
.button {
&[aria-pressed='true'] {
background-color: gainsboro;
}
&[disabled] {
opacity: 0.4;
}
}
Структура стилизации
Нам нравится этот подход, потому что он создаёт чёткую структуру:
- Класс стилизует базовый элемент
- Атрибут стилизует состояние
Для изоляции стилей мы используем CSS Modules, который встроен в большинство React-фреймворков (Next.js, Create React App, Remix).
Заключение
Эти техники не требуют только чистого CSS — их можно комбинировать с CSS-in-JS или препроцессорами. Но с новыми фун кциями вроде нативной вложенности и относительных цветов необходимость в дополнительных инструментах снижается.
В студии PSI мы стилизуем весь наш сайт именно этими техниками. Если хотите увидеть, как это работает на реальных компонентах, изучите наш исходный код.
Итог: Современный CSS достаточно мощный, чтобы решать задачи динамической стилизации без тяжеловесных библиотек. Попробуйте наш подход в следующем проекте!
