
CSS бывает непредсказуемым — и зачастую виновата именно специфичность. Автор статьи объясняет, почему ваши стили могут вести себя неожиданным образом и почему лучше разобраться в специфичности, чем полагаться на флаги !important
.
CSS — штука дикая, реально дикая. И хитрая. Но давайте поговорим конкретно о специфичности.
При написании CSS практически невозможно избежать разочарования, когда стили применяются не так, как ожидалось — это особенность специфичности. Вы применили стиль, он сработал, а позже пытаетесь переопределить его другим стилем и... ничего, он вас игнорирует. Опять же, дело в специфичности.
Да, есть вариант прибегнуть к флагу !important
, однако, как и все разработчики до нас, мы знаем, что это рискованно и нежелательно. Гораздо лучше разобраться в механике специфичности, чтобы потом не сражаться с расставленными кругом флагами важности.
Специфичность 101
Многие разработчики понимают концепцию специфичности по-разному.
Основная идея специфичности заключается в том, что алгоритм каскадирования CSS, используемый браузерами, определяет, какое объявление стилей применить, если два или больше правил относятся к одному и тому же элементу.
Подумайте об этом. По мере расширения проекта увеличиваются и проблемы специфичности. Предположим, разработчик А добавляет определение .cart-button
, и кнопка выглядит хорошо на боковой панели, но с небольшим изменением. Затем разработчик Б добавляет определение .cart-button .sidebar
, и с этого момента любые будущие изменения, примененные к .cart-button
, могут быть переопределены правилами .cart-button .sidebar
. И вот начинается война специфичностей.
Я занимался разработкой CSS достаточно долго и видел разные стратегии, используемые разработчиками для управления проблемами специфичности.
/* Традиционный подход */
#header .nav li a.active { color: blue; }
/* БЭМ */
.header__nav-item--active { color: blue; }
/* Утилитарные классы */
.text-blue { color: blue; }
/* Каскадные слои */
@layer components {
.nav-link.active { color: blue; }
}
Все эти методы отражают различные стратегии контроля или хотя бы поддержания специфичности CSS:
- БЭМ — пытается упростить специфичность через явность.
- Утилитарные классы CSS — стремится обойти специфичность путем атомарности.
- Каскадные слои CSS — управляют специфичностью путём организации стилей в упорядоченных группах.
Мы рассмотрим все три подхода и посмотрим, как они справляются со специфичностью.
Мои отношения со специфичностью
Раньше мне казалось, что я понимаю всю картину специфичности CSS. Обычное правило звучало так: встроенный стиль важнее идентификатора, который важнее класса, который важнее тега. Но внимательно прочитав документацию по работе алгоритма каскадирования по настоящему открыло мне глаза.
Вот код, над которым я работал в старом проекте клиента:
/* Унаследованный код */
#main-content .product-grid button.add-to-cart {
background-color: #3a86ff;
color: white;
padding: 10px 15px;
border-radius: 4px;
}
/* Здесь ещё около сотни строк другого кода */
/* Мой новый CSS */
.btn-primary {
background-color: #4361ee; /* Новый фирменный цвет */
color: white;
padding: 12px 20px;
border-radius: 4px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
Глядя на этот код, становится ясно, что класс .btn-primary
по специфичности никак не сможет конкурировать с ранее написанной цепочкой селекторов. В терминах спецификации CSS первому селектору присваивается оценка 1, 2, 1
: один балл за идентификатор, два балла за два класса и один балл за элемент-селектор. Второй же селектор оценивается как 0, 1, 0
, поскольку состоит всего лишь из одного класса.
Да, у меня были варианты:
- Я мог использовать
!important
для свойств в классе.btn-primary
, чтобы переопределить более сильный селектор, однако стоит лишь раз применить его и придется применять его повсеместно. Поэтому лучше избегать такого пути. - Я мог попытаться повысить специфичность, но по моему мнению это было бы слишком жестоко по отношению к следующему разработчику (которым вполне могу оказаться я сам).
- Я мог изменить стили существующего кода, но это только усугубляет проблему специфичности:
#main-content .product-grid .btn-primary {
/* Редактирую стили непосредственно здесь */
}
В конце концов, я переписал весь CSS заново.
Когда появилась возможность использовать вложенность, я попробовал контролировать специфичность таким образом:
.profile-widget {
// ... другие стили
.header {
// ... стили заголовка
.user-avatar {
border: 2px solid blue;
&.is-admin {
border-color: gold; /* Это становится .profile-widget .header .user-avatar.is-admin */
}
}
}
}
И вот так, совершенно случайно, я создал правила высокой специфичности. Именно настолько легко и естественно мы можем незаметно дрейфовать к усложнению специфичности.
Чтобы избежать множества проблем подобного рода, я придерживаюсь одного принципа: поддерживать уровень специфичности как можно ниже. А когда сложность селекторов превращается в запутанные цепочки — я пересматриваю всю структуру целиком.
БЭМ: оригинальная система
Методология Блок-Элемент-Модификатор (сокращённо БЭМ) существует уже давно. Это методологическая система написания CSS-кода, которая заставляет вас сделать каждую иерархию стилей явной и чёткой.
/* Блок */
.panel {}
/* Элемент, зависящий от блока */
.panel__header {}
.panel__content {}
.panel__footer {}
/* Модификатор, меняющий стиль блока */
.panel--highlighted {}
.panel__button--secondary {}
Когда я впервые столкнулся с БЭМ, мне показалось это потрясающим, несмотря на противоположное мнение некоторых людей, считавших такой подход некрасивым. Меня нисколько не смущали двойные дефисы и подчёркивания, поскольку они делали мой CSS предсказуемым и упрощённым.
Специфичность в БЭМ
Посмотрите на эти примеры.
Без БЭМ:
/* Специфичность: 0, 3, 0 */
.site-header .main-nav .nav-link {
color: #472EFE;
text-decoration: none;
}
/* Специфичность: 0, 2, 0 */
.nav-link.special {
color: #FF5733;
}
С использованием БЭМ:
/* Специфичность: 0, 1, 0 */
.main-nav__link {
color: #472EFE;
text-decoration: none;
}
/* Специфичность: 0, 1, 0 */
.main-nav__link--special {
color: #FF5733;
}
Видите, насколько БЭМ делает код предсказуемым? Все селекторы равнозначны, что облегчает сопровождение и расширение проекта. Если захочу добавить кнопку внутрь .main-nav
, я просто напишу класс .main-nav__btn
. А если нужна кнопка в отключённом состоянии (модификатор), достаточно создать класс .main-nav__btn--disabled
. Специфичность остаётся низкой, ведь не нужно повышать её искусственно или бороться с каскадностью — достаточно написать новый класс.
Принцип именования классов в БЭМ гарантирует изоляцию компонентов друг от друга, благодаря чему классы вроде .card__title
никогда случайно не пересекутся с .menu__title
.
Недостатки БЭМ
Идея БЭМ мне нравится, однако она далека от идеала, и многие пользователи заметили недостатки:
- Имена классов становятся реально длинными.
<div class="product-carousel__slide--featured product-carousel__slide--on-sale">
<!-- вот это да! -->
</div>
- Повторное использование компонентов может не стоять на первом месте, что несколько противоречит идеологии нативного CSS. Должна ли кнопка внутри карточки называться
.card__button
или повторно использовать глобальный класс.button
? В первом случае стили дублируются, во втором нарушается строгая модель БЭМ. - Одна из основных проблем разработки программного обеспечения постепенно становится реальной — необходимость давать названия вещам. Уверен, вы уже знакомы с подобными сложностями.
БЭМ хороша, но иногда вам нужно проявлять гибкость. Гибридная система (например, используем БЭМ для базовых компонентов, но в остальных случаях используем более простые классы) всё равно позволяет поддерживать специфичность на минимально необходимом уровне.
/* Базовая кнопка без БЭМ */
.button {
/* Стили кнопки */
}
/* Кнопка компонента с использованием БЭМ */
.card__footer .button {
/* Незначительные переопределения */
}
Утилетарные классы: Специфичность путём её отсутствия
Этот подход также называется атомарным CSS. И он полностью избегает специфичность.
<button class="bg-red-300 hover:bg-red-500 text-white py-2 px-4 rounded">
Кнопка
</button>
Идея утилитарных классов заключается в том, что каждый такой класс имеет одинаковую специфичность, равную одному селектору класса. Каждый класс представляет собой крошечное CSS-свойство, выполняющее одну единственную задачу.
p-2
? Отступ, ничего больше. text-red
? Красный цвет текста. text-center
? Выравнивание текста по центру. Это похоже на конструктор LEGO, но применительно к стилям. Вы складываете классы друг поверх друга до тех пор, пока не получите нужный внешний вид.
Как утилитарные классы справляются со специфичностью
Утилитарные классы сами по себе не решают проблему специфичности, а скорее доводят идею низкой специфичности БЭМ до крайности. Практически все утилитарные классы имеют самый низкий возможный уровень специфичности (0, 1, 0
). Благодаря этому переопределение стилей становится простым делом: если нужен больший отступ, достаточно заменить .p-2
на .p-4
.
Другой пример:
<button class="bg-orange-300 hover:bg-orange-700">
На кнопку можно навести курсор
</button>
Если добавляется другой класс, например, hover:bg-red-500
, то для определения того, какой стиль будет использован, важную роль играет порядок следования. Таким образом, хотя утилитарные классы позволяют избежать специфичности, другие части алгоритма каскадирования CSS, а именно, порядок следования, вступают в игру, где побеждает селекстор, объявленный последним.
Недостатки утилитарных классов
Наиболее распространённая проблема утилитарных классов состоит в том, что они делают код некрасивым. Честно говоря, я согласен с этим мнением. Но возможность представить, как выглядит компонент, даже не глядя на рендеринг, бесценна.
Также есть аргумент относительно повторного использования, ведь вы повторяетесь каждый раз заново. Однако когда замечаешь повторяющиеся элементы, проще превратить эту часть в многократно используемый компонент.
Утилитарные классы действительно ограничены в отношении специфичности:
- Если изменится брендовый цвет, который является глобальной переменной, и вы глубоко погружены в базу кода, нельзя просто изменить одно значение и ожидать, чтобы остальные автоматически последовали примеру нативного CSS.
- Естественная родительско-дочерняя связь, существующая в нативном CSS, теряется из-за поведения атомарных утилитарных классов.
- Некоторые утверждают, что HTML должен оставаться разметкой, а CSS отвечать исключительно за оформление. Теперь же приходится просматривать большее количество разметки, и если вы решите очистить код:
<!-- Слишком длинно -->
<div class="p-4 bg-yellow-100 border border-yellow-300 text-yellow-800 rounded">
<!-- Лучше? -->
<div class="alert-warning">
Вот так мы снова оказались в ситуации написания CSS-кода. Круг замкнулся.
Из моего опыта работы с утилитарными классами можно сказать, что они лучше всего подходят для:
- Скорость Создание разметки, её оформление и быстрое получение результата.
- Предсказуемость Утилитарные классы делают именно то, что заявлено.
Каскадные слои: Специфичность по дизайну
Теперь становится интересно. БЭМ предлагает структуру, утилитарные классы обеспечивают скорость, а каскадные слои CSS предоставляют нам нечто важное: контроль.
Итак, каскадные слои (@layer
) группируют стили и объявляют порядок следования групп независимо от специфичности правил.
Рассмотрим набор независимых стилей:
button {
background-color: orange; /* Специфичность: 0, 0, 1 */
}
.button {
background-color: blue; /* Специфичность: 0, 1, 0 */
}
#button {
background-color: red; /* Специфичность: 1, 0, 0 */
}
/* При любом раскладе кнопка будет красной */
Но с помощью правила @layer
мы можем сделать приоритетным селектор класса .button
. Мы можем задать порядок важности спецификаторов следующим образом:
@layer utilities, defaults, components;
@layer defaults {
button {
background-color: orange; /* Специфичность: 0, 0, 1 */
}
}
@layer components {
.button {
background-color: blue; /* Специфичность: 0, 1, 0 */
}
}
@layer utilities {
#button {
background-color: red; /* Специфичность: 1, 0, 0 */
}
}
Из-за работы механизма @layer
, класс .button
победит, поскольку слой компонентов имеет наивысший приоритет, несмотря на то, что у селектора #button
выше специфичность. Таким образом, до проверки обычных правил специфичности учитывается порядок слоёв.
Теперь вы можете намеренно переопределять селекторы идентификатора простым классом, даже не используя !important
.
Особенности каскадных слоев
Вот несколько моментов, заслуживающих внимания при работе с каскадными слоями CSS:
- Специфичность всё ещё важна.
!important
работает иначе, чем ожидалось внутри@layer
(они действуют наоборот).- Слои
@layer
относятся не к конкретному селектору, а к свойствам стиля.
@layer base {
.button {
background-color: blue;
color: white;
}
}
@layer theme {
.button {
background-color: red;
/* Здесь отсутствует свойство цвета, поэтому белый цвет из базового слоя сохраняется */
}
}
- Слоями
@layer
легко злоупотребить. Уверен, где-то есть разработчик с более чем 20 объявлениями слоёв, превратившихся в монстра.
Сравнение всех трёх подходов
Для тех, кто предпочитает читать коротко, вот сравнение трех методов: БЭМ, утилитарные классы и каскадные слои CSS.
Функция | БЭМ | Утилитарные классы | Каскадные слои |
---|---|---|---|
Основная идея | Компоненты пространства имен | Классы с одной целью | Порядок каскадирования управления |
Контроль специфичности | Низкий и плоский | Полностью отсутствует | Абсолютный контроль благодаря превосходству слоев |
Читаемость кода | Четкая структура благодаря именованию | Непонятна, если незнаком с названиями классов | Чётко, если соблюдается структура слоёв |
Вербальность HTML | Умеренные имена классов (могут стать длинными) | Много маленьких классов, быстро накапливаются | Нет прямого влияния, остаётся только в CSS |
Организация CSS | По компоненту | По свойству | По приоритетному порядку |
Кривая обучения | Требует понимания конвенций | Необходимо знание названий утилит | Легко освоить, но требует глубокого понимания CSS |
Зависимость от инструментов | Только чистый CSS | Часто зависит от сторонних решений, например, Tailwind | Родной CSS |
Простота рефакторинга | Высокая | Средняя | Низкая |
Лучшее применение | Системы дизайна | Быстрое создание прототипов | Устаревший код или сторонние коды, нуждающиеся в переопределении |
Поддержка браузерами | Все | Все | Все (кроме IE) |
Среди трёх подходов каждый имеет свою область наилучшего применения:
- БЭМ подходит лучше всего, когда:
- Есть чёткая система дизайна, которой нужно придерживаться,
- Команда придерживается разных взглядов относительно CSS (БЭМ может стать золотой серединой), и
- Стили менее подвержены утечкам между компонентами.
- Утилитарные классы работают лучше всего, когда:
- Нужно быстро построить проект, как прототип или минимально жизнеспособные продукты (MVP), и
- Используется фреймворк на основе компонентов JavaScript, такой как React.
- Каскадные слои наиболее эффективны, когда:
- Работа ведётся над устаревшими проектами, где нужен полный контроль специфичности,
- Необходима интеграция сторонних библиотек или стилей из различных источников, и
- Разрабатывается большое сложное приложение или проекты с долгосрочным сопровождением.
Если бы мне пришлось выбирать или ранжировать их, я бы предпочёл утилитарные классы совместно со слоями каскадов перед использованием БЭМ. Но это моё личное мнение!
Где пересекаются подходы (как они могут работать вместе)
Из всех трёх, каскадные слои стоит рассматривать как дирижёра, поскольку они могут эффективно взаимодействовать с двумя другими стратегиями. @layer
— фундаментальное положение архитектуры каскада CSS, тогда как БЭМ и утилитарные классы являются методологиями контроля поведения каскада.
/* Каскадные слои + БЭМ */
@layer components {
.card__title {
font-size: 1.5rem;
font-weight: bold;
}
}
/* Каскадные слои + Утилитарные классы */
@layer utilities {
.text-xl {
font-size: 1.25rem;
}
.font-bold {
font-weight: 700;
}
}
Использование же БЭМ совместно с утилитарными классами приведёт лишь к конфликтам:
<!-- Что-то здесь не так -->
<div class="card__container p-4 flex items-center">
<p class="card__title text-xl font-bold">Что-то здесь кажется неправильным</p>
</div>
Но раскрою карты: я разработчик, ориентированный на использование утилитарных классов. И большинство утилитарных фреймворков используют @layer
за кулисами (например, Tailwind). Так что эти два подхода уже давно идут рука об руку.
Тем не менее, я вовсе не испытываю неприязнь к БЭМ. Я много использовал этот подход и продолжаю использовать, если возникает необходимость. Просто придумывать названия — утомительное занятие.
Однако мы все разные, и ваши предпочтения могут отличаться от моих. Это прекрасно, ведь пространство веб-разработки даёт возможность выбрать путь, который вам ближе. Несколько маршрутов ведут к одному месту назначения.
Заключение
Итак, есть ли истинный победитель среди БЭМ, утилитарных классов и каскадных слоёв CSS при контроле специфичности в каскаде?
Во-первых, каскадные слои однозначно являются самой мощной функцией CSS за последние годы. Их нельзя путать ни с БЭМ, ни с утилитарными классами, которые представляют собой стратегии, а не части набора функций CSS.
Именно поэтому мне нравится мысль комбинирования либо БЭМ, либо утилитарных классов с каскадными слоями. В любом случае основная цель заключается в сохранении низкой специфичности и применении каскадных слоёв для определения приоритета стилей.