Олег Марков
Стилизация компонентов component-styling
Введение
Стилизация компонентов (component-styling) стала одним из ключевых аспектов фронтенд-разработки. Когда вы начинаете строить интерфейсы на основе компонентов, быстро становится понятно, что просто писать глобальные CSS-правила уже недостаточно. Появляются конфликты имен, сложно управлять темами, труднее переиспользовать стили и поддерживать код.
Давайте разберем, какие подходы сегодня используются для стилизации компонентов, чем они отличаются, какие у каждого плюсы и минусы и как выбрать стратегию под ваш проект. Я покажу вам примеры кода и укажу на нюансы, о которые чаще всего спотыкаются разработчики.
Базовые принципы стилизации компонентов
Инкапсуляция стилей
Главная идея component-styling — инкапсулировать стили вместе с компонентом. То есть:
- компонент сам "знает", какие классы он использует;
- стили не "протекают" случайно в другие части приложения;
- изменение стилей одного компонента не ломает другие.
Стилизация становится частью интерфейсного контракта компонента: вы явно определяете, какие варианты отображения компонент поддерживает (например, размер, цвет, состояние disabled) и как это описано в API.
Локальная и глобальная область видимости
Чтобы управлять стилями в компонентном подходе, важно понимать, где стили локальные, а где глобальные.
- Локальные стили применяются только внутри компонента и не влияют на остальной код.
- Глобальные стили определяют общие правила: базовую типографику, переменные, reset/normalize и т.п.
Хороший стиль — свести глобальные стили к "фундаменту" (база, переменные, темы) и максимально локализовать все, что относится к конкретному компоненту.
Подходы к стилизации компонентов
Классический CSS и методологии (BEM и подобные)
Самый понятный старт — обычные CSS/SCSS файлы плюс дисциплина именования.
BEM как основа предсказуемости
Давайте посмотрим на пример простого компонента кнопки с BEM:
<!-- Компонент кнопки с BEM-классами -->
<button class="button button--primary button--size-md">
Отправить
</button>
/* Базовый класс блока */
.button {
/* Основной стиль кнопки */
display: inline-flex;
align-items: center;
justify-content: center;
padding: 8px 16px;
border-radius: 4px;
border: none;
cursor: pointer;
}
/* Модификатор "primary" */
.button--primary {
/* Основной цвет фона для "primary" варианта */
background-color: #2563eb;
color: #ffffff;
}
/* Модификатор размера */
.button--size-md {
/* Размер шрифта и дополнительные отступы */
font-size: 14px;
}
/* Состояние disabled */
.button--disabled {
/* Ослабленный вид и отключенный курсор */
opacity: 0.6;
cursor: not-allowed;
}
Здесь вы видите три сущности:
- блок (
.button); - модификаторы варианта (
.button--primary); - модификаторы размера (
.button--size-md).
Такой подход легко реализовать без дополнительных библиотек и он уже делает стили более предсказуемыми.
Плюсы и минусы подхода
Плюсы:
- просто начать;
- работает без сборщиков (подходит даже для старых проектов);
- легко читать и искать стили.
Минусы:
- нет жесткой инкапсуляции — стили остаются глобальными;
- возможны конфликты имен при росте проекта;
- сложнее автоматизировать переиспользование токенов и тем.
CSS Modules
CSS Modules — это шаг вперед по сравнению с обычным CSS: имена классов становятся "локальными" для файла.
Смотрите, я покажу вам, как это выглядит в проекте с React.
Файл стилей:
/* Button.module.css */
/* Базовый класс кнопки */
.button {
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 4px;
border: none;
cursor: pointer;
}
/* Вариант "primary" */
.primary {
background-color: #2563eb;
color: #ffffff;
}
/* Вариант "outline" */
.outline {
background-color: transparent;
border: 1px solid #2563eb;
color: #2563eb;
}
/* Размеры */
.small {
padding: 4px 8px;
font-size: 12px;
}
.medium {
padding: 8px 16px;
font-size: 14px;
}
Файл компонента:
// Button.jsx
// Импортируем стили как объект
import styles from './Button.module.css';
export function Button({ variant = 'primary', size = 'medium', children }) {
// Собираем классы исходя из пропсов
const className = [
styles.button, // базовый стиль
styles[variant], // variant -> styles.primary или styles.outline
styles[size] // size -> styles.small или styles.medium
].join(' '); // объединяем в одну строку
return (
<button className={className}>
{children}
</button>
);
}
Бандлер (например, Webpack) преобразует имена классов во что-то вроде Button_button__3aX7f, то есть делает их уникальными. Снаружи вы уже не сможете случайно переопределить этот класс, если не будете импортировать модуль стилей.
Плюсы:
- реальная локальность стилей;
- все еще обычный CSS (работают SCSS, PostCSS и т.п.);
- понятно, как мигрировать с классического CSS.
Минусы:
- сложнее писать условные стили с большим количеством комбинаций классов;
- темизацию и общие токены приходится продумывать отдельно.
CSS-in-JS (styled-components, Emotion, другие)
CSS-in-JS объединяет стили и логику компонента в одном месте. Вместо отдельных файлов стилей вы пишете их рядом с компонентом в JavaScript/TypeScript.
Покажу вам, как это выглядит на примере styled-components:
// Button.tsx
// Импортируем функцию styled
import styled from 'styled-components';
// Описываем стилизованный компонент
const Button = styled.button`
/* Базовый стиль кнопки */
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 4px;
border: none;
cursor: pointer;
padding: 8px 16px;
font-size: 14px;
/* Используем проп "variant" для выбора фона */
background-color: ${({ variant }) =>
variant === 'primary' ? '#2563eb' : 'transparent'};
color: ${({ variant }) =>
variant === 'primary' ? '#ffffff' : '#2563eb'};
border: ${({ variant }) =>
variant === 'outline' ? '1px solid #2563eb' : 'none'};
`;
// Экспортируем компонент
export function AppButton({ variant = 'primary', children }) {
return (
<Button variant={variant}>
{children}
</Button>
);
}
Здесь вы видите, что пропсы (variant) прямо используются в стилях. Это делает динамические стили очень удобными: можно подставлять значения из темы, состояния компонента, пропсов.
Плюсы CSS-in-JS
- высокая динамичность — стили легко зависят от пропсов и состояния;
- из коробки есть механизм темизации (особенно в styled-components, Emotion);
- стили действительно "живут" рядом с компонентом и легко переносимы.
Минусы
- дополнительная сложность в конфигурации и производительности (особенно без серверного рендеринга);
- зависимость от конкретной библиотеки и ее экосистемы;
- иногда сложнее отладка и интеграция с дизайнерами, привыкшими к "классическому" CSS.
Utility-first CSS и Tailwind
Utility-first подход (как в Tailwind CSS) предлагает другой взгляд на component-styling: вместо того, чтобы описывать стили для компонента в одном месте, вы "собираете" его внешний вид из маленьких утилитных классов.
Пример:
<!-- Кнопка, стилизованная только с помощью утилитных классов Tailwind -->
<button
class="inline-flex items-center justify-center
rounded-md px-4 py-2 text-sm
bg-blue-600 text-white
hover:bg-blue-700
disabled:opacity-60 disabled:cursor-not-allowed"
>
Отправить
</button>
Как видите, весь вид компонента описан прямо в разметке. Это удобно, когда у вас есть заранее настроенная дизайн-система в Tailwind (токены цвета, отступы, типографика).
Плюсы:
- высокая скорость разработки (особенно для прототипов);
- строгая привязка к дизайн-системе (вы почти не пишете "произвольные" значения);
- меньше контекстных переключений между файлами.
Минусы:
- разметка может стать перегруженной классами;
- переиспользование стиля одного и того же компонента требует дополнительных слоев абстракции (например, создания оберток-компонентов);
- некоторая зависимость от Tailwind-конфига и подхода к проектированию.
Логика вариаций и состояний компонента
API стилей через пропсы
Один из ключевых вопросов component-styling — как вы описываете "варианты" и "состояния" компонента.
Пример с пропсами variant и size:
// Button.tsx
// Описываем интерфейс пропсов кнопки
type ButtonVariant = 'primary' | 'secondary' | 'outline';
type ButtonSize = 'sm' | 'md' | 'lg';
interface ButtonProps {
variant?: ButtonVariant;
size?: ButtonSize;
disabled?: boolean;
children: React.ReactNode;
}
// Пример компонента с управлением классами
export function Button({
variant = 'primary',
size = 'md',
disabled = false,
children
}: ButtonProps) {
// Собираем классы в зависимости от пропсов
const classes = [
'button', // базовый класс
`button--${variant}`, // класс варианта
`button--size-${size}`, // класс размера
disabled ? 'button--disabled' : '' // класс состояния
]
.filter(Boolean) // убираем пустые строки
.join(' '); // собираем в одну строку
return (
<button className={classes} disabled={disabled}>
{children}
</button>
);
}
Такой подход делает связи между API компонента и стилями прозрачными: дизайнеру и разработчику проще обсуждать, какие варианты есть и что они означают.
Управление состояниями через классы и атрибуты
Состояния (hover, focus, active, disabled, selected) лучше явно отражать в стилях.
Пример на чистом CSS:
/* Стили интерактивных состояний */
.button:hover:not(.button--disabled) {
/* Подсветка при наведении */
background-color: #1d4ed8;
}
/* Состояние "active" при клике */
.button:active:not(.button--disabled) {
/* Более темный цвет при нажатии */
background-color: #1e40af;
}
/* Фокус с клавиатуры */
.button:focus-visible {
/* Видимая обводка для доступности */
outline: 2px solid #1d4ed8;
outline-offset: 2px;
}
При использовании фреймворков удобно комбинировать CSS-селекторы и логические классы, например, .is-active, .is-open, которые выставляются компонентом в зависимости от состояния.
Темизация и дизайн-системы
CSS-переменные как фундамент тем
Темизация — это возможность менять визуальное оформление (например, светлая/темная тема) без переписывания всех стилей. Очень удобный способ сделать это — использовать CSS-переменные (CSS Custom Properties).
Давайте разберемся на примере базовых токенов:
/* Базовая тема по умолчанию */
:root {
/* Цвет фона страницы */
--color-bg: #ffffff;
/* Цвет основного текста */
--color-text: #111827;
/* Основной акцентный цвет */
--color-primary: #2563eb;
}
/* Темная тема */
[data-theme="dark"] {
/* Меняем фон и текст */
--color-bg: #0f172a;
--color-text: #e5e7eb;
--color-primary: #3b82f6;
}
Теперь вы можете использовать эти переменные во всех компонентах:
/* Компонент кнопки, использующий токены темы */
.button {
background-color: var(--color-primary); /* цвет из темы */
color: var(--color-bg); /* текст контрастный к фону */
}
/* Фон страницы */
.page {
background-color: var(--color-bg);
color: var(--color-text);
}
Компонент переключения темы просто меняет атрибут data-theme на корневом элементе, и все компоненты автоматически перенастраиваются.
Темизация в CSS-in-JS
Если вы используете styled-components или Emotion, у вас есть дополнительный инструмент — объект темы, передаваемый через контекст.
Пример с styled-components:
// theme.ts
// Описываем светлую и темную темы
export const lightTheme = {
colors: {
bg: '#ffffff',
text: '#111827',
primary: '#2563eb'
}
};
export const darkTheme = {
colors: {
bg: '#0f172a',
text: '#e5e7eb',
primary: '#3b82f6'
}
};
// Button.tsx
// Импортируем styled из библиотеки
import styled from 'styled-components';
// Создаем стилизованную кнопку c использованием темы
export const Button = styled.button`
padding: 8px 16px;
border-radius: 4px;
border: none;
cursor: pointer;
/* Берем цвета из темы */
background-color: ${({ theme }) => theme.colors.primary};
color: ${({ theme }) => theme.colors.bg};
`;
// App.tsx
// Подключаем ThemeProvider для передачи темы
import { ThemeProvider } from 'styled-components';
import { Button } from './Button';
import { lightTheme, darkTheme } from './theme';
export function App() {
const isDark = false; // Здесь можно использовать состояние или настройки пользователя
return (
<ThemeProvider theme={isDark ? darkTheme : lightTheme}>
<Button>Кнопка</Button>
</ThemeProvider>
);
}
Теперь вы можете переключать темы на уровне приложения, а все компоненты будут подстраиваться под текущий объект theme.
Структурирование стилей и архитектура
Разделение слоев: foundation, components, overrides
Хорошая практика — разделять стили на несколько логических слоев:
Foundation (фундамент)
Базовые токены и общие правила:- переменные;
- шрифты;
- reset/normalize;
- базовая типографика.
Components (компоненты)
Стили отдельных элементов интерфейса:- кнопки;
- поля ввода;
- карточки;
- модальные окна.
Overrides (переопределения)
Локальные правки для конкретных экранов:- особенности layout;
- специфические стили под конкретный кейс, не подходящие в базовый компонент.
Пример организации файлов в проекте:
- styles/
- foundation/
- variables.css
- typography.css
- reset.css
- components/
- Button.module.css
- Input.module.css
- pages/
- home.css
- dashboard.css
- foundation/
Такой подход помогает не смешивать фундаментальные вещи и частные настройки.
Переиспользуемость и композиция стилей
Композиция — ключевой прием в component-styling. Вместо копирования стилей вы комбинируете уже существующие.
Пример: у вас есть базовая кнопка и вы хотите создать компонент "иконка-кнопка".
SCSS:
/* BaseButton.scss */
/* Базовый класс кнопки */
.base-button {
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 4px;
border: none;
cursor: pointer;
padding: 8px 16px;
}
/* IconButton.scss */
/* Импортируем стили базовой кнопки */
@use './BaseButton.scss' as base;
/* Наследуем базовый стиль */
.icon-button {
@extend .base-button; // используем стили из базовой кнопки
padding: 4px; // уменьшаем отступы под иконку
width: 32px;
height: 32px;
}
CSS Modules:
/* IconButton.module.css */
.iconButton {
composes: button from './Button.module.css'; /* берем стили из другого модуля */
padding: 4px;
width: 32px;
height: 32px;
}
Так вы избегаете дублирования и обеспечиваете единый вид всех кнопок в интерфейсе.
Стилизация в контексте фреймворков
React
В экосистеме React стилизация компонентов — особенно частая тема. Подходов много, но у большинства есть общие рекомендации:
- Старайтесь, чтобы компонент "самодостаточно" включал свои стили (через импорт CSS Modules, styled-components или другие механизмы).
- Для сложных компонентов делите структуру на "layout" и "view" части, чтобы поменять внешний вид не затрагивая бизнес-логику.
Пример компонента с CSS Modules и логикой:
// Card.module.css
.card {
border-radius: 8px;
border: 1px solid #e5e7eb;
padding: 16px;
background-color: var(--color-bg);
}
.header {
font-weight: 600;
margin-bottom: 8px;
}
.content {
font-size: 14px;
color: var(--color-text);
}
// Card.tsx
// Импортируем стили
import styles from './Card.module.css';
interface CardProps {
title: string;
children: React.ReactNode;
}
// Простой компонент карточки
export function Card({ title, children }: CardProps) {
return (
<div className={styles.card}>
<div className={styles.header}>{title}</div>
<div className={styles.content}>{children}</div>
</div>
);
}
Здесь структура и стили находятся рядом, а внешний код просто использует компонент Card, не думая о том, какие там классы внутри.
Vue (SFC и scoped-стили)
В Vue Single File Components стили часто пишут прямо в файле компонента, используя scoped, чтобы привязать их только к этому компоненту.
Пример:
<!-- Button.vue -->
<template>
<!-- Привязываем класс в зависимости от варианта -->
<button
class="button"
:class="[`button--${variant}`]"
:disabled="disabled"
>
<slot />
</button>
</template>
<script>
export default {
name: 'AppButton',
props: {
variant: {
type: String,
default: 'primary'
},
disabled: {
type: Boolean,
default: false
}
}
}
</script>
<style scoped>
/* Базовый стиль кнопки */
.button {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 8px 16px;
border-radius: 4px;
border: none;
cursor: pointer;
}
/* Вариант "primary" */
.button--primary {
background-color: #2563eb;
color: #ffffff;
}
/* Вариант "secondary" */
.button--secondary {
background-color: #e5e7eb;
color: #111827;
}
</style>
Атрибут scoped гарантирует, что эти стили будут применяться только к разметке этого компонента.
Производительность и оптимизация стилей
Критические стили и загрузка
При component-styling важно следить, чтобы количество и размер стилей не росли неконтролируемо.
Кратко о практиках:
- по возможности разделяйте стили по страницам (code splitting) вместе с компонентами, чтобы не загружать все сразу;
- выносите "критические" стили (например, для above-the-fold контента) в отдельный блок или файл;
- не создавайте тысячи уникальных классов в рантайме (это может быть проблемой при неаккуратном использовании CSS-in-JS).
Минимизация дублирования
Если вы замечаете, что похожие значения (цвета, отступы) повторяются в разных компонентах — это сигнал к выделению токенов:
- цвета;
- размеры шрифтов;
- значения border-radius;
- spacing-сетка (отступы).
Чем раньше вы выделите эти значения в общие переменные (CSS-переменные, JS-константы, токены дизайн-системы), тем проще будет дальше развивать и менять стили.
Тестируемость и поддерживаемость
Визуальные регрессионные тесты
Поскольку компонентные стили тесно связаны с внешним видом, удобный инструмент — визуальные регрессионные тесты (например, Chromatic, Loki, Percy). Они помогают понять, что изменение стиля не сломало другие компоненты.
Логика простая:
- Вы описываете компоненты в Storybook.
- Инструмент снимает скриншоты для разных состояний.
- При изменениях сравниваются "до" и "после".
Это особенно полезно при большой дизайн-системе.
Договоренности в команде
Стилизация компонентов — это не только код, но и договоренности:
- какая методология именования классов используется;
- где и как объявляются токены;
- когда допустимо писать "локальные" стили внутри страницы, а когда нужно обновить базовый компонент;
- чем вы стилизуете (CSS Modules, CSS-in-JS, Tailwind).
Чем понятнее эти правила, тем меньше "хаоса" накапливается в кодовой базе.
Стилизация компонентов — это область, где пересекаются дизайн, архитектура и практика. Вы можете комбинировать подходы: например, использовать CSS-переменные для темизации, CSS Modules для инкапсуляции стилей и Tailwind-утилиты для быстрых прототипов. Главное — держать стили как часть API компонента и думать о них так же структурно, как о логике.
Частозадаваемые технические вопросы по теме и ответы
Как совместить Tailwind с компонентным подходом, чтобы не захламлять разметку?
Создавайте обертки-компоненты, в которых Tailwind-классы инкапсулированы. Например, в React:
// Создаем компонент Button с предопределенными Tailwind-классами
export function Button({ children, className = '', ...props }) {
const base =
'inline-flex items-center justify-center rounded-md px-4 py-2 text-sm';
// Позволяем расширять базовый набор классов через проп className
return (
<button className={`${base} ${className}`} {...props}>
{children}
</button>
);
}
Так вы используете Tailwind внутри компонента, а снаружи работаете с чистым API.
Как организовать переопределение стилей компонента в разных проектах без форка?
Вводите слой "skin" или "theme overrides". Например:
- базовая библиотека экспортирует компоненты с классами/токенами;
- в проекте-использователе вы подключаете свой файл
theme-overrides.css, который переопределяет только переменные и некоторые классы; - договаривайтесь, какие классы/переменные считаются "частью контракта" и могут переопределяться.
Главное — не переопределять все подряд, а фиксировать стабильные точки расширения.
Как правильно типизировать варианты стилей в TypeScript, чтобы не допускать опечаток?
Определите типы вариантов как union-тип и используйте их в пропсах:
// Описываем допустимые варианты
type Variant = 'primary' | 'secondary' | 'outline';
// Пропсы компонента с использованием типа варианта
interface ButtonProps {
variant?: Variant;
}
Если вы пишете variant="primay", TypeScript сразу покажет ошибку. То же можно сделать для размеров, состояний и других вариантов.
Как избежать дублирования токенов между CSS и JS при темизации?
Храните токены в одном формате (например, JSON) и генерируйте из него:
- CSS-файл с переменными;
- TS/JS-модуль с объектом темы.
Можно использовать скрипт (Node.js) или инструменты токенов (например, Style Dictionary). Тогда источник правды один, а формы представления — разные.
Как подружить компонентные стили и CSS-анимации, чтобы они не ломались при рефакторинге?
Выделяйте анимации в отдельный слой:
- отдельные файлы
animations.cssили раздел в foundation; - используйте понятные, стабильные имена
@keyframes(например,fade-in,slide-up), которые считаются частью публичного контракта; - компоненты опираются не на "случайные" keyframes, а на этот слой, который реже меняется.
Так при рефакторинге компонентных стилей вы не рискуете случайно удалить или переименовать общую анимацию.
Постройте личный план изучения Vue до уровня Middle — бесплатно!
Vue — часть карты развития Frontend
100+ шагов развития
30 бесплатных лекций
300 бонусных рублей на счет
Бесплатные лекции
Все гайды по Vue
Лучшие курсы по теме

Vue 3 и Pinia
Антон Ларичев
TypeScript с нуля
Антон Ларичев