Специфичность и каскад вглубь
Разбираемся, по каким правилам браузер выбирает один CSS-объявление из десятка конкурирующих — и почему «не применяется мой стиль» почти всегда про специфичность.
Специфичность — это вес селектора, который браузер сравнивает у конкурирующих правил, чтобы решить, какое из них победит. Чем «адреснее» селектор, тем он специфичнее.
Когда два правила задают одно и то же свойство одному элементу, должно остаться одно. Этот выбор называют каскадом, и он не случаен: у каждого правила есть приоритет, вычисляемый по чётким шагам. Понимать их обязательно — иначе правки превращаются в гадание, а код обрастает !important и дублями.
Зачем это на практике
Типичная боль большого проекта: вы пишете .button { background: green; }, а кнопка остаётся синей. Открываете DevTools — ваше правило зачёркнуто. Причина почти всегда одна: где-то есть селектор специфичнее. Без модели специфичности разработчик начинает «лечить симптом» — добавляет !important или ещё более длинный селектор, и через полгода стили читаются как минное поле. Знание правил каскада позволяет писать предсказуемый CSS, где победитель очевиден заранее.
Как считается специфичность: тройка (a, b, c)
Специфичность селектора — это три числа, которые удобно записывать как (a, b, c):
- a — количество
#idв селекторе; - b — количество классов
.class, атрибутов[type=text]и псевдоклассов:hover,:focus; - c — количество тегов
div,pи псевдоэлементов::before,::after.
Сравнение идёт слева направо, как у версий: сначала по a, при равенстве — по b, затем по c. Один #id «тяжелее» любого числа классов, а один класс — тяжелее любого числа тегов. Универсальный селектор *, комбинаторы (>, +, ~, пробел) и псевдокласс :where() в счёт не идут — добавляют ноль.
/* (0,0,1) — один тег */
p { color: gray; }
/* (0,1,1) — класс + тег */
p.intro { color: black; }
/* (0,2,1) — два класса + тег */
.card p.intro { color: navy; }
/* (1,0,0) — один id, перевешивает всё выше */
#main { color: teal; }
Если все четыре правила нацелены на один абзац, выиграет #main: его (1,0,0) больше любого (0,…,…). Уберите id — победит .card p.intro с (0,2,1).
Считаем на примере
Возьмём селектор nav#top ul.menu li a:hover. Считаем по категориям: id — один (#top) → a=1; классы и псевдоклассы — .menu и :hover → b=2; теги — nav, ul, li, a → c=4. Итог: (1, 2, 4). Этот селектор перевесит, например, .menu a:hover с (0, 2, 1), потому что у первого есть id.
Источники стилей и их приоритет
Специфичность сравнивают внутри одного источника и слоя. Сам каскад сначала отбирает правила по более крупным критериям — это «происхождение и важность». Упрощённо порядок (от слабого к сильному):
| Уровень | Что это |
| Стили браузера по умолчанию | встроенная таблица user-agent |
| Обычные правила автора | ваш CSS-файл |
Inline-стиль style="..." | атрибут прямо на элементе |
!important автора | важные правила вашего CSS |
!important пользователя | важные правила пользовательской таблицы (доступность) |
Inline-стиль фактически имеет вес, который не побить обычным селектором из файла — представляйте его как специфичность (1,0,0,0) с лишним разрядом слева. Именно поэтому правка через атрибут style почти всегда «выигрывает» и так мешает поддержке.
!important и почему его избегают
!important выдёргивает объявление из обычного соревнования специфичности и поднимает его в более высокий слой важности. Работает — но ценой предсказуемости.
.alert { color: red !important; } /* перебьёт даже #id-правило */
Проблема в эскалации: чтобы переопределить чужой !important, нужен ещё один !important с большей специфичностью. Так файл превращается в гонку вооружений, где обычные правила вообще перестают что-либо менять. Допустимые исключения — узкие утилитарные классы (.is-hidden { display: none !important; }) и перекрытие стилей сторонних виджетов, которые сами всё лепят через важность. В прикладном коде компонентов !important — почти всегда сигнал, что архитектура селекторов поехала.
Наследование — отдельный механизм
Наследование часто путают с каскадом, но это разные вещи. Каскад выбирает правило для того же элемента; наследование передаёт значение от родителя к потомку, когда у потомка свойство не задано вовсе. Наследуются в основном «текстовые» свойства: color, font-family, font-size, line-height, text-align. А «коробочные» — margin, padding, border, background — не наследуются.
body { color: #333; font-family: system-ui; }
/* у вложенного <p> цвет и шрифт унаследуются автоматически,
даже если для p ничего не написано */
Унаследованным значением управляют ключевые слова: inherit (взять у родителя принудительно), initial (сбросить к начальному значению свойства), unset (наследуемое свойство ведёт себя как inherit, ненаследуемое — как initial).
Порядок объявления — последний судья
Если у двух правил одинаковые источник, важность и специфичность — выигрывает то, что объявлено позже в коде. Это и есть «каскад» в исходном смысле слова: вода течёт сверху вниз.
.btn { color: blue; }
.btn { color: green; } /* победит: оно ниже, специфичность та же */
Практическое следствие: порядок подключения файлов и порядок строк имеет значение. Поэтому общие/сбрасывающие стили подключают раньше, а компонентные и темовые — позже, чтобы они могли при равной специфичности взять верх.
Как это работает под капотом
Когда браузер строит страницу, для каждого элемента он собирает все подходящие правила из всех таблиц стилей. Затем прогоняет их через каскад: группирует по происхождению и важности, внутри группы сортирует по специфичности, а при равенстве — по порядку появления. На выходе для каждого CSS-свойства остаётся ровно один победитель — так формируется итоговый «computed style». Важно, что соревнование идёт по каждому свойству отдельно: у одного элемента color может прийти из одного правила, а font-size — из другого. В DevTools вы видите ровно этот результат: зачёркнутые строки — проигравшие объявления, незачёркнутые — победители.
Частые ошибки
- Лечат «не применяется» через
!important. Сначала посмотрите в DevTools, какой селектор побеждает и почему — обычно достаточно поднять специфичность на один класс, а не звать важность. - Гонятся за специфичностью длинными селекторами.
.page .content .card .titleработает, но в следующий раз перебить его будет ещё сложнее. Плоские короткие селекторы (см. урок про BEM) поддерживать проще. - Путают каскад и наследование. Если у элемента не задан
color, он не «проигрывает каскад» — он просто наследует цвет родителя. А вотbackgroundне унаследуется никогда. - Полагаются на порядок, который легко сломать. Победа «по последней строке» исчезнет, если кто-то переставит импорты или добавит правило выше. Не стройте логику на хрупком порядке — управляйте специфичностью осознанно.
- Забывают про inline-стиль. Если правило из файла не побеждает несмотря на высокую специфичность — проверьте, нет ли на элементе атрибута
style: его обычным селектором не перебить.
Итоги
- Специфичность — тройка
(a, b, c): id / классы-атрибуты-псевдоклассы / теги-псевдоэлементы; сравнение слева направо, как у версий. - Каскад сначала разбирается с происхождением и важностью, и только потом сравнивает специфичность; внутри равной специфичности судит порядок объявления.
!importantподнимает правило в более высокий слой важности, но провоцирует эскалацию — в коде компонентов его лучше избегать.- Наследование — отдельный механизм: текстовые свойства передаются потомкам, коробочные — нет; управляется через
inherit/initial/unset. - При полном равенстве выигрывает правило, написанное ниже, — поэтому порядок подключения стилей имеет значение.