Специфичность (specificity) – механизм каскада CSS: когда к одному элементу подходят несколько правил с одним и тем же свойством, браузер сравнивает вес их селекторов и применяет самое специфичное. Понятие существует со времён первых версий CSS, а актуальные правила подсчёта – включая вес функциональных псевдоклассов – закреплены в спецификации Selectors Level 4. Понимание специфичности снимает главную боль вёрстки – «стиль прописан в файле, но элемент его игнорирует».
Как это работает
Специфичность – не одно число, а триада из трёх счётчиков A-B-C. Браузер раскладывает каждую часть селектора в одну из трёх колонок и записывает результат вида 1-2-1.
| Колонка | Что считается | Примеры | Вклад |
|---|---|---|---|
| A | ID-селекторы | #menu, #app | 1-0-0 |
| B | Классы, селекторы атрибутов, псевдоклассы | .card, [type=radio], :hover | 0-1-0 |
| C | Селекторы типов (тегов) и псевдоэлементы | p, ::before | 0-0-1 |
Например, :root #app input:required – это 1-2-1: один ID, два псевдокласса и один тег. Порядок частей внутри селектора не важен: .class#id и #id.class дают одинаковые 1-1-0.
Триада и «баллы 100/10/1» – в чём разница
В русскоязычных статьях до сих пор гуляет устаревшая модель: «ID – 100 баллов, класс – 10, тег – 1; складываем и сравниваем суммы». MDN и web.dev прямо предостерегают от неё. Специфичность – не десятичное число, а тройка, которая сравнивается попозиционно, слева направо: сначала колонка A, при равенстве – B, затем C. Переноса «разрядов» между колонками не существует: один ID тяжелее любого количества классов.
| Селектор | Триада | «Баллы» (устаревшая модель) | Итог по триаде |
|---|---|---|---|
#promo | 1-0-0 | 100 | Побеждает: колонка A старше |
| Цепочка из 11 классов | 0-11-0 | «110» | Проигрывает, хотя «баллов» больше |
.a .b .c [id=el] | 0-4-0 | «40» | Проигрывает #el (1-0-0) |
Что не влияет на вес
- Универсальный селектор
*– даёт 0-0-0. - Комбинаторы (пробел,
>,+,~) – не считаются:div > pиdiv pравны, 0-0-2. :where()– всегда 0-0-0 независимо от аргументов.- Наследование и близость в DOM – унаследованное значение всегда проигрывает прямому таргетированию, а
body h1иhtml h1равны: «близость» предка не учитывается.
Инлайн-стили (атрибут style) и !important – тоже не специфичность, а другие уровни каскада, которые оцениваются до её сравнения: инлайн-стиль перебивает любой селектор, а !important перекидывает объявление в отдельный приоритетный слой. При конфликте двух !important побеждает уже более специфичный селектор. MDN называет !important антипаттерном; для управляемых оверрайдов рекомендованы каскадные слои @layer: при конфликте правил из разных слоёв специфичность вообще не сравнивается, роль играет порядок слоёв.
Функциональные псевдоклассы :is(), :not() и :has() собственного веса не имеют, но получают специфичность самого специфичного селектора из своих аргументов: :is(p, #fakeId) – 1-0-0, p:not(#fakeId) – 1-0-1. А :nth-child(An+B of S) даёт 0-1-0 за сам псевдокласс плюс вес самого специфичного селектора в списке S.
Зачем это нужно
Первая практическая задача – диагностика. Если стиль написан, но не применяется, чаще всего его перебивает более специфичное правило: DevTools показывают проигравшие объявления перечёркнутыми в панели Styles, а триаду селектора – в подсказке при наведении. Вторая – архитектура: чем ровнее специфичность по проекту, тем предсказуемее оверрайды и меньше соблазн тянуться за !important.
Управлять весом можно в обе стороны:
- Снизить – записать ID как селектор атрибута:
[id=x]даёт 0-1-0 вместо 1-0-0; обернуть селектор в:where()и обнулить вес – стандартный приём резетов и CSS-библиотек, чтобы пользовательские стили перебивали их одним классом. - Повысить без !important – продублировать ID (
#id#id– 2-0-0) или добавить:not(#fakeId): «накачка» триады, оправданная как точечный оверрайд чужого кода. - Держать ровной – методологии вроде БЭМ строят стили на одиночных классах (0-1-0), исключая войны специфичности в командной работе.
Пример
* { } /* 0-0-0 */
p::first-line { } /* 0-0-2: тег + псевдоэлемент */
[type=radio]:hover { } /* 0-2-0 */
div.outer p { } /* 0-1-2 */
:root #app input:required { } /* 1-2-1 */
/* Конфликт: побеждает ID, а не «больше частей» */
#el { color: green; } /* 1-0-0 – применится */
.a .b .c [id=el] { color: red; } /* 0-4-0 */
/* :is / :not / :has – самый специфичный аргумент */
:is(p, #fakeId) { } /* 1-0-0 */
h1:has(+ h2, > #fakeId) { } /* 1-0-1 */
/* :where – всегда ноль */
:where(#theme) a { } /* 0-0-1 */
footer a { } /* 0-0-2 – перебивает предыдущее */Разберём конфликт из середины примера. Селектор .a .b .c [id=el] выглядит длиннее и «точнее», и по устаревшей модели баллов набрал бы 40 против 100 – но решает не длина, а старшая колонка: у #el есть ID, поэтому текст будет зелёным. В работе с Битрикс-шаблонами это типовая ситуация: стили ядра или готового шаблона компонента написаны через ID-обёртки, и перебить их одним классом не получится. Рабочие варианты – селектор атрибута [id=...] вместо #id в своём коде или осознанное повышение специфичности, но не !important, который через полгода придётся перебивать ещё одним !important.