Специфичность (specificity) – механизм каскада CSS: когда к одному элементу подходят несколько правил с одним и тем же свойством, браузер сравнивает вес их селекторов и применяет самое специфичное. Понятие существует со времён первых версий CSS, а актуальные правила подсчёта – включая вес функциональных псевдоклассов – закреплены в спецификации Selectors Level 4. Понимание специфичности снимает главную боль вёрстки – «стиль прописан в файле, но элемент его игнорирует».
Специфичность селектора
Определение
Специфичность селектора – механизм каскада CSS, определяющий, какое из конфликтующих правил применит браузер. Считается триадой A-B-C (ID, классы, теги) и сравнивается попозиционно: один ID тяжелее любого числа классов.
4 мин 40 Обновлено 12 августа
Как это работает
Специфичность – не одно число, а триада из трёх счётчиков 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.
Частые вопросы
Как посчитать специфичность селектора вручную?
Разложите селектор в три счётчика A-B-C: A – ID-селекторы (#id), B – классы, селекторы атрибутов и псевдоклассы (.card, [type=radio], :hover), C – теги и псевдоэлементы (p, ::before). Например, :root #app input:required – это 1-2-1. Универсальный селектор * и комбинаторы не считаются вовсе.
Правда ли, что специфичность – это сумма баллов 100, 10 и 1?
Нет, это устаревшее упрощение из старых статей – MDN и web.dev прямо предостерегают от него. Триада сравнивается попозиционно: сначала счётчик ID, при равенстве – классы, затем теги. Переноса «разрядов» нет: селектор из одиннадцати классов (0-11-0) проиграет одному ID (1-0-0), хотя «по баллам» набрал бы 110 против 100.
Что не влияет на специфичность селектора?
Универсальный селектор * даёт 0-0-0, комбинаторы (пробел, дочерний, соседние) не считаются вовсе, а :where() всегда равен нулю независимо от аргументов. Инлайн-стили и !important – тоже не специфичность, а отдельные, более приоритетные уровни каскада. Близость в DOM не учитывается: body h1 и html h1 равны.
Почему !important перебивает даже ID-селектор и почему это антипаттерн?
!important перекидывает объявление в отдельный, более приоритетный слой каскада, который оценивается до сравнения специфичности. MDN называет его антипаттерном: перебить такой стиль можно только ещё одним !important, и проект скатывается в войну приоритетов. Для управляемых оверрайдов рекомендованы каскадные слои @layer.
Какая специфичность у :is(), :not() и :has()?
Сам псевдокласс веса не добавляет, но правило получает специфичность самого специфичного селектора из его аргументов. Так, :is(p, #fakeId) считается как 1-0-0, а p:not(#fakeId) – как 1-0-1. Это правило закреплено в спецификации Selectors Level 4.
Чем :where() отличается от :is()?
Только специфичностью: :is() берёт вес самого специфичного аргумента, а :where() всегда даёт 0-0-0. Поэтому :where() используют в резетах и CSS-библиотеках – их стили легко переопределить одним классом, без борьбы за приоритет.
Что побеждает при полностью равной специфичности?
Последнее правило в порядке исходного кода: браузер применит то, что объявлено ниже – в том же файле или в файле, подключённом позже. Порядок частей внутри селектора при этом не важен: .class#id и #id.class равны. Именно поэтому порядок подключения стилей в проекте имеет значение.
Связанные термины
Материалы по теме
Что почитать дальше по этой теме