10 мая 2025 г.·6 мин чтения

Архитектура CSS для продуктовых команд, которая масштабируется с ростом функционала

Архитектура CSS для продуктовых команд удерживает стили локальными, токены общими и снижает хаос при росте функционала. Узнайте настройку, которая остаётся аккуратной по мере того, как в продукте появляются новые люди и фичи.

Архитектура CSS для продуктовых команд, которая масштабируется с ростом функционала

Почему CSS в продуктовых приложениях быстро становится беспорядочным

Продуктовые приложения живут годами, и многие люди правят одни и те же экраны. Страница настроек может начинаться с одного инженера, потом её дорабатывает другая команда, добавляется правка поддержки и быстрый эксперимент для одного клиента. Код сохраняет каждое решение, в том числе поспешные.

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

Главная проблема — общий CSS без чётких границ. Селектор вроде .card h3 или .table td кажется безобидным, когда приложение маленькое. Через пару месяцев он попадает на страницы аккаунтов, в админку, всплывающие окна и формы, которые делали разные люди по разным причинам. Тогда мелкая фиксация в одной фиче протекает в другую.

Давление по времени усугубляет ситуацию. Когда баг блокирует релиз, люди делают быстрый оверрайд, более длинный селектор или используют !important. Такая правка может жить месяцами. Следующий разработчик не понимает, зачем она нужна, и добавляет ещё один слой поверх.

Через какое‑то время никто не хочет трогать экран, если только нет крайней необходимости. Даже осторожные инженеры начинают действовать по‑защитному: не чистят код, копируют старые паттерны и вносят минимальные изменения, чтобы закрыть тикет.

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

Тогда и начинаются «войны стилей». Один хочет общий класс, другой — локальный оверрайд, третий поднимает специфичность селектора, чтобы успеть выпустить фичу. Дискуссия обычно не про вкус — она про то, что код больше не показывает владельца, и поэтому любое изменение кажется рискованным.

Что продуктовым командам нужно от CSS

Продукт меняется каждую неделю. Биллинг, онбординг, админка и отчёты движутся с разной скоростью, поэтому стили должны позволять этим областям расти, не протекая друг в друга. Если два разработчика редактируют разные части приложения в один день, им не стоит проводить вторую половину дня, исправляя отступы или состояния кнопок друг друга.

Хороший CSS должен быть скучным в лучшем смысле. Новый сотрудник или подрядчик должен понять по имени класса, что он делает, за несколько секунд. Когда нейминг превращается в головоломку, код‑ревью замедляются, и даже мелкие правки кажутся рискованными.

Большинству команд нужна одна и та же базовая настройка: локальные стили для компонентов, небольшой общий слой для цветов и отступов, понятные правила для состояний (ошибка, загрузка) и адаптивность, которая не зависит от случайных переопределений на страницах. Это совпадает с реальной продуктовой работой, где таблица, форма и модал часто живут на одном экране.

Темы тоже важны. Светлая/тёмная тема, брендирование аккаунтов и правки ради доступности должны приходить из консистентного слоя токенов, а не из одноразовых фиксов внутри каждой фичи. Если команда меняет отступы в формах, она должна обновить токен один раз, а не искать по двадцати файлам.

Короткие ревью — хороший тест. Пулл‑реквест должен ясно показывать, что изменилось и где это останавливается. Если разработчик обновляет карточку плана, ревьюерам не нужно беспокоиться о потоке оформления заказа, меню пользователя и трёх старых страницах, которые всё ещё используют неопределённый .card.

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

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

Что оставлять глобальным, а что держать локально

Глобальный CSS должен быть маленьким и предсказуемым. Кладите туда только то, что одинаково нужно всем экранам: сбросы (resets), общие токены и, возможно, несколько базовых правил для текста, если весь продукт использует их одинаково. Если правило служит одной фиче, одной странице или особому случаю — ему не место в глобальном слое.

Дизайн‑токены живут в этом общем слое, потому что дают приложению общий язык. Цвета, отступы, радиусы, тени и размеры шрифтов можно держать в одном месте. Токен глобален. Решение о том, как карточка, таблица или модал используют токен, остаётся внутри компонента.

Макет и состояния держите вместе с кодом компонента. Padding, gap, ширины, hover‑стили, выбранные состояния, состояния ошибки и пустые состояния должны путешествовать вместе с кнопкой, формой, таблицей или выезжающей панелью, к которым они принадлежат. Это содержит изменения. Команда может обновить форму биллинга, не беспокоясь о странице отчётов или админке, которые случайно используют похожий класс.

Правила, относящиеся только к странице, держите рядом со страницей. Некоторые экраны требуют особой сетки, липкой боковой панели или плотных отступов — это нормально. Ошибка — переносить такие одноразовые правки в общие файлы просто потому, что так быстрее на ближайшие пять минут.

Эта псевдо‑оптимизация вызывает большинство конфликтов. Кто‑то подкручивает общее правило табов, чтобы подписи помещались на странице биллинга. Позже другой человек меняет то же общее правило для настроек. Теперь обе страницы тянут в разные стороны, и никто не помнит, почему глобальный селектор ведёт себя странно на одном брейкпойнте.

Простой тест работает хорошо: если вы удалите компонент или страницу завтра, должен ли исчезнуть и её CSS? Если да — держите его локальным. Если правило нужно всему продукту — оставьте его глобальным. Такое разграничение экономит огромное количество переделок, когда фичи начинают накапливаться.

Используйте дизайн‑токены как общий слой

Команды работают быстрее, когда цвета и отступы перестают быть личным выбором. Дизайн‑токены дают всем общий набор имён для визуальных решений, так что новая фича не превращается в спор «какой серый» или «сколько паддинга».

Начните с малого. Большинству команд сначала нужны несколько групп токенов: цвета, отступы, радиусы, типографика и тени. Это покрывает кнопки, формы, таблицы, модалы, алерты и пустые состояния. Если вы начнёте со 80 токенов, люди перестанут их использовать и вернутся к случайным значениям.

Именуйте токены по роли, а не по сырым значениям. color-text-primary живёт дольше. blue-600 нет, потому что в момент смены фирменного цвета имя перестаёт отражать смысл. Та же логика для отступов и типографики: space-3 легче переиспользовать, чем 12px. radius-card прослужит дольше, чем 8px, когда стили карточки поменяются.

Токены стоят между бренд‑решениями и кодом компонентов. Таблица биллинга должна просить surface, border-subtle и text-muted. Ей не важно, светло‑серый это сегодня или чуть темнее через квартал. Это делает стили компонентов спокойнее и проще в поддержке.

Темы тоже становятся проще. Используйте одинаковые имена токенов для светлой и тёмной тем, вместо двух разных систем нейминга. color-bg-surface должен существовать в обеих темах, даже если значение меняется. Разработчики используют тот же класс или переменную — меняется только слой темы.

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

Простое правило команды: компоненты могут использовать токены, но не должны придумывать новые сырые значения, если система не покрывает потребность. Такая привычка предотвращает многие споры о стилях до их начала.

Выберите правило области и держитесь его

Clean Up Shared CSS
Get help moving risky global rules into safer component boundaries.

Большинству команд не нужна хитрая система CSS. Им нужно одно правило, которого все будут придерживаться каждый день, даже когда дедлайны поджимают.

Выберите подход и придерживайтесь. CSS Modules хорошо работают в React и Next.js. Встроенные scoped‑стили подходят для Vue и Svelte. Если стек не поддерживает это, заведите строгое правило нейминга и не нарушайте его ради «одной страницы».

Простая группа правил подойдёт большинству:

  • У каждого компонента один ясный корневой класс.
  • Селектируйте только элементы, которые живут внутри этого корня.
  • Не лезьте в DOM другого компонента.
  • Меняйте вид с помощью пропов или вариантов, а не случайными оверрайд‑классами.

Этот корневой класс — граница. Если у вас есть SettingsPanel, дайте ему один класс, например .settingsPanel, и держите дочерние селекторы в том же файле: .settingsPanelHeader, .settingsPanelBody, .settingsPanelActions. Как только другой компонент начнёт стилизовать эти классы извне, граница нарушена.

То же относится к вложенности. Селектор вроде .settingsPanel .button кажется безобидным, но теперь панель контролирует, как ведёт себя кнопка внутри неё. Через месяц кто‑то переиспользует эту кнопку в модальном окне, скопирует оверрайд, и в итоге у вас окажется две версии одной кнопки с немного разными отступами.

Варианты решают большинство задач без драмы. Если кнопке нужен компактный вариант, добавьте вариант, например size="small" или variant="quiet". Положите стиль в сам компонент кнопки. Родитель выбирает версию, а не переписывает внутренности кнопки.

Токены всё ещё находятся выше этих правил: они задают общие значения вроде цвета, отступа и радиуса. Правила области решают, где эти значения применяются. Это разные задачи, и их смешивание обычно приводит к беспорядку.

Если команда спорит о CSS каждый спринт, проблема чаще всего не в вкусе — никто не договорился, где начинается компонент и где он заканчивается.

Как настраивать пошагово

Начните с того CSS, который у вас уже есть, а не с идеальной системы, которую вы хотели бы иметь. Стабильная настройка обычно вырастает из одного прохода по очистке и нескольких мелких рефакторов, которые команда может повторять в обычной работе.

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

Откройте глобальные стили и разложите каждое правило по трём корзинам: active, risky или dead. Active — правила, которые реально влияют на живые экраны. Risky — правила, которые тянут слишком далеко, например .page div button. Dead — правила, у которых нет видимой роли и которые можно удалить.

Перенесите повторяющиеся значения в дизайн‑токены, прежде чем трогать структуру компонентов. Начните с цветов, отступов, размеров шрифтов, радиусов и теней. Если на пяти экранах используются чуть разные серые для одинакового текста, выберите один токен и используйте его.

Дальше рефакторьте по группам компонентов. Возьмите что‑то ограниченное, например формы биллинга или карточки настроек аккаунта, и переместите их стили в локальные файлы компонентов. Оставьте остальной код базы в покое, пока группа не станет стабильной.

Линтеры помогают: блокируйте шаблоны, которые уже вызывают проблемы — глубокие селекторы, глобальные переопределения страниц и новые глобальные правила без ясной причины. Это экономит время на ревью и снижает количество спорных PR.

Напишите короткое руководство команды на основе реального кода, а не теории. Покажите, где разрешены глобалы, как называются токены и как выглядит чистый файл стилей компонента. Это поместится на одной странице. Не нужен огромный дизайн‑система — достаточно деталей, чтобы два разработчика сделали одинаковый выбор при стилизации следующей фичи.

Практическое правило помогает: новый UI сначала делайте локальным. Если стиль действительно общий — докажите это использованием в двух‑трёх местах, и только потом продвигайте его в токен или общий паттерн. Такой порядок держит базу спокойной и мешает возврату старого глобального CSS.

Пример страницы биллинга

Improve Review Workflow
Oleg can pair front-end cleanup with AI-assisted code review for a small team.

Команда добавляет раздел биллинга в SaaS‑продукт. На странице есть выбор плана, форма оплаты, таблица счетов и сводка использования. Плохо организованный CSS обычно проявляется здесь быстро: у каждого блока свои состояния, и страницу трогают разные люди.

Команда начинает с токенов, а не с кастомных значений. Отступы используют ту же шкалу, что и в остальном приложении, так что gap над полем формы и padding внутри ячейки таблицы берутся из одного набора правил. Цвета тоже из токенов: текст, приглушённый текст, граница, поверхность, danger и success. Никто не вставляет случайный hex, чтобы биллинг выглядел «по‑другому».

Каждый блок биллинга хранит свои состояния локально. Форма оплаты сама обрабатывает неверные данные карты внутри компонента формы. Таблица счетов владеет пустой строкой, кнопкой retry и скелетоном загрузки. Сводка подписки держит бейджи об обновлении и уведомления о просрочке локально, вместо того чтобы добавлять глобальные классы вроде .error, .warning или .table-empty, которые позже влияют на другие экраны.

Обычно в такой настройке получается несколько отдельных компонентов: PlanSelector, PaymentMethodForm, InvoiceTable, UsageSummary и BillingNotice. Они могут читать одни и те же токены, но их селекторы остаются в собственных файлах или модулях. Такое простое решение обычно стареет лучше, чем хитрая схема.

Перед слиянием ветки команда проверяет не только happy path. Открывают страницу с реалистичными данными по счетам, тестируют загрузку, пустые и ошибочные состояния. Пробуют длинные названия планов, текст о неудавшейся оплате и широкую таблицу на маленьком экране.

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

Ошибки, которые запускают «войны стилей"

Review Your Front End
Oleg can audit your CSS boundaries, tokens, and shared styles with your team.

CSS ломается обычно мелко и по‑обычному. Одна короткая хитрость попадает в PR, никто не хочет блокировать фичу, и через три спринта команда спорит о том, чьи стили "неправильные".

Обычная ошибка — переиспользование старого класса, потому что он кажется похожим. Стиль бейджа из биллинга копируется в настройки аккаунта, потом кто‑то меняет бейдж для нового состояния счёта, и страница настроек смещается. Класс выглядел удобным, но нес с собой старый смысл.

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

Плохие имена токенов наносят ущерб постепенно. Когда токены называются по экрану, это привязывает дизайн к странице. Имена вроде billingBlue или signupCardBorder стареют плохо при редизайне. Токены лучше, когда описывают роль: surface, text, border, danger.

Дедлайны делают крайние состояния лёгкой добычей для игнорирования. Потом начинается лоскутная починка: кнопка выглядит нормально в дефолтном состоянии, но никто не проверил disabled, loading, error, длинный текст или пустые данные. Разработчик добавляет один селектор для одного состояния, затем ещё один для другого — и компонент становится хрупким.

Общие компоненты дрейфуют, когда в них запихивают верстку страницы. У карточки появляется лишний верхний margin для одной страницы. Модал получает фиксированную ширину, потому что одна форма выглядит тесно. Вскоре общий компонент подходит только для страницы, которая его «исказила».

Ранние признаки: селекторы, которые таргетят разметку другой фичи; токены, привязанные к одной странице; общие компоненты с вшитыми маргинами или ширинами; поспешные фиксы для состояний; и повторное использование старых классов с новым смыслом.

Большинство «войн стилей" — это на самом деле войны за владение. Держите стили локальными, токены — общими и общими по роли, а решения по раскладке — рядом со страницей, которая в них нуждается. Когда границы ясны, спорят гораздо меньше.

Быстрая проверка и следующие шаги

Хороший CSS кажется почти скучным в неделю релиза. Новые фичи появляются, старые экраны сохраняют форму, и никто не проводит день, гоняясь за цветом кнопки, который случайно поменялся.

Начните с глобального слоя. Если кто‑то трогал resets, селекторы по элементам, утилиты или базовые стили форм, откройте несколько несвязанных страниц и посмотрите на дрейф: биллинг, настройки, таблицы и пустые состояния быстро показывают проблемы.

Короткий проход ревью ловит большинство проблем. Проверяйте компонент в обычном состоянии, hover, error и disabled. Измените несколько типичных ширин экрана и посмотрите, где плохо переносятся подписи, табы и ячейки таблицы. Попробуйте длинный текст в кнопках, алертах и полях форм. Удалите классы, которые ни на одном экране больше не используются. Удалите токены, у которых нет реальных ссылок в коде.

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

Когда вы берётесь за такую область, держите правило простым. Вынесите общие значения (spacing, color, radius) в токены. Держите макет и правила состояний рядом с компонентом‑владельцем. Если стиль нужен только одной странице, не выносите его в глобальный файл «на всякий случай».

Команды приходят к конфликтам, когда никто не знает, где должно жить правило. Запишите несколько простых правил, используйте их два‑три спринта и меняйте только если та же боль появляется снова.

Если хотите второе мнение по такой очистке, Oleg Sotnikov на oleg.is работает со стартапами и маленькими командами как Fractional CTO. Его подход практичен: ясные фронтенд‑правила, более компактная инженерная настройка и AI‑первичные рабочие процессы, которые помогают командам выпускать фичи, не превращая CSS в общую проблему.

Часто задаваемые вопросы

What should I do first if our app CSS already feels messy?

Начните с того, что рассортируйте глобальные стили на три группы: правила, которые ещё реально используются; правила, которые «тянут» слишком далеко (например, .page div button); и правила, которые можно удалить. После этого вынесите повторяющиеся значения — отступы, цвета, радиусы — в токены прежде, чем переписывать стили компонентов.

What should stay global in a product app?

Держите глобальную часть минимальной: сбросы (resets), дизайн-токены и, при необходимости, несколько базовых правил для текста, если все экраны используют их одинаково. Стили фич, раскладки страниц и одноразовые правки держите рядом с компонентом или страницей, которой они принадлежат.

Should design tokens use raw color names like blue-600?

Нет. Именуйте токены по роли, а не по значению. Например, color-text-primary или color-bg-surface лучше, чем blue-600, потому что при изменении бренда не придётся переименовывать токен.

Are CSS Modules enough, or do we need a more complex naming system?

Для большинства команд на React и Next.js — да. CSS Modules обеспечивают понятные границы без лишнего усилия. Самое важное — правило: оставляйте селекторы внутри компонента и не стилизуйте один компонент извне другого.

How do I know if a style should be local or shared?

Простой тест работает хорошо: если удалить этот компонент или страницу завтра, исчезнут ли вместе с ними и связанные CSS‑правила? Если да — держите их локальными. Если правило нужно всему продукту — вынесите его в общий слой.

How should we handle button or form variants without messy overrides?

Родитель выбирает вариант, но стили находятся внутри самого компонента. Если кнопке нужен компактный вид, добавьте проп size или variant, например size="small" или variant="quiet", вместо того чтобы писать внешние селекторы вроде .settingsPanel .button.

How many design tokens do we actually need at the start?

Начинайте с малого. Большинству команд хватает групп токенов: цвета, отступы (spacing), типографика, радиусы и тени. Если создать слишком много токенов на старте, люди перестанут их использовать и снова вернутся к произвольным значениями.

What mistakes usually start CSS style wars?

Обычно проблемы возникают, когда старые классы используют с новым смыслом, когда одна команда переопределяет разметку другой, или когда правила для одной страницы выносятся в общий компонент. Ещё одна частая причина — забытые крайние состояния (loading, empty, error, disabled, длинный текст).

How can we review CSS changes without breaking unrelated screens?

Сначала проверяйте обычное состояние, затем hover, error, disabled, loading и empty на той же странице. Смените несколько типичных ширин экрана и попробуйте длинные тексты в таблицах, алертах и полях форм — это ловит дрейф верстки до того, как он распространится.

When should we ask an outside expert to help with CSS architecture?

Привлекайте внешнюю помощь, когда команда постоянно «обходит» старые CSS‑проблемы, вместо того чтобы их решать, или когда никто не может договориться, где должны жить правила. Fractional CTO вроде Oleg Sotnikov (oleg.is) помогает сформулировать простые фронтенд‑правила, ужать общий слой и ускорить работу команды без постоянных конфликтов о стилях.