Специфичность селектора

Специфичность селектора – механизм каскада CSS, определяющий, какое из конфликтующих правил применит браузер. Считается триадой A-B-C (ID, классы, теги) и сравнивается попозиционно: один ID тяжелее любого числа классов.

4 минуты чтения

Специфичность (specificity) – механизм каскада CSS: когда к одному элементу подходят несколько правил с одним и тем же свойством, браузер сравнивает вес их селекторов и применяет самое специфичное. Понятие существует со времён первых версий CSS, а актуальные правила подсчёта – включая вес функциональных псевдоклассов – закреплены в спецификации Selectors Level 4. Понимание специфичности снимает главную боль вёрстки – «стиль прописан в файле, но элемент его игнорирует».

Как это работает

Специфичность – не одно число, а триада из трёх счётчиков A-B-C. Браузер раскладывает каждую часть селектора в одну из трёх колонок и записывает результат вида 1-2-1.

Триада специфичности A-B-C
КолонкаЧто считаетсяПримерыВклад
AID-селекторы#menu, #app1-0-0
BКлассы, селекторы атрибутов, псевдоклассы.card, [type=radio], :hover0-1-0
CСелекторы типов (тегов) и псевдоэлементыp, ::before0-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 тяжелее любого количества классов.

Почему модель «баллов» даёт неверный ответ
СелекторТриада«Баллы» (устаревшая модель)Итог по триаде
#promo1-0-0100Побеждает: колонка 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 равны. Именно поэтому порядок подключения стилей в проекте имеет значение.

Материалы по теме

Валентина Меланина

Нужна консультация?

Разберу ваш сайт и покажу точки роста

Если хотите понять, как этот термин применить к вашему проекту — начнём с аудита.