Главная / Статьи / Как каскад выбирает победителя: важность, специфичность и порядок источников

Как каскад выбирает победителя: важность, специфичность и порядок источников

Узнайте, как браузеры решают конфликты между заявлениями CSS, как интерпретировать специфичность через четырехэтапное сравнение, и почему правила для состояния hover и !important так часто ставят вас в тупик.

2447 слов

Когда несколько правил CSS нацелены на один и тот же элемент и устанавливают одно и то же свойство, браузер не может применить их все. Ему нужен детерминированный способ выбора ровно одного из вариантов определения, и этот процесс объясняет целую категорию ошибок вида «почему мой стиль игнорируется?». К концу этого руководства вы сможете рассчитывать специфичность селектора, предсказывать, какое определение будет применено, и отлаживать сложные случаи, такие как правило :hover, которое никогда не срабатывает, без использования атрибута !important.

Место этого процесса в цепочке отрисовки

В общих чертах браузер преобразует маркировку и стили в пиксели через ряд этапов. HTML парсится в DOM, CSS — в CSSOM, затем они объединяются в дерево отрисовки, после чего процессы разметки и отрисовки создают то, что вы видите:

HTML
  ↓
DOM
  ↓
CSS
  ↓
CSSOM
  ↓
Render Tree
  ↓
Layout
  ↓
Paint
  ↓
Pixels

Внутри процесса обработки CSS скрыты три взаимосвязанных вопроса. Во-первых, когда существует несколько конкурирующих объявлений, какое из них побеждает? Во-вторых, после выбора победителя к чему на самом деле приводит его значение? В-третьих, что происходит, когда у элемента вообще нет значения для той или иной свойства? Ответами являются принцип каскадирования (основанный на специфичности), обработка значений и наследование. Их часто рассматривают как независимые темы, но в браузере это последовательные этапы одной и той же процедуры. Этот гид посвящен первому вопросу. Если вы хотите узнать больше о том, что происходит после обработки стилей, прочитайте как браузер отрисовывает элементы и какое место занимает React.

Правила, объявления и конкурирующие значения

Сначала немного лексики. Правило CSS состоит из селектора, за которым следует блок объявлений:

.button {
  background-color: blue;
}

В этом правиле .button — это селектор, background-color: blue; — объявление, background-color — свойство, а blue — значение. Значение в том виде, в котором оно написано, называется заявленным значением.

Настоящий стильовой лист часто содержит несколько объявлений для одного и того же свойства у одного и того же элемента. Одно правило может относиться ко всем кнопкам:

button {
  background-color: red;
}

в то время как другие, возможно, в отдельном файле, относятся к кнопкам в целом и к одной конкретной кнопке по её id:

button {
  background-color: blue;
}

#submit {
  background-color: green;
}

Если один элемент <button id="submit"> соответствует всем трем правилам, какой цвет фона ему следует присвоить? Решение этой задачи лежит в основе каскадирования. Оно урегулирует конфликты, учитывая степень важности каждого правила, специфичность его селектора и порядок их появления. После учета степени важности обычно именно специфичность определяет итоговый результат.

В полной версии каскадирования, описанной в спецификации, также учитывается происхождение таблицы стилей (стандартные настройки браузера, пользовательские стили, стили автора), а в современном CSS — уровни каскадирования, объявленные с помощью @layer. Для обычных пользовательских таблиц стилей без уровней каскадирования ключевыми факторами для принятия решений являются степень важности, специфичность и порядок исходных правил.

Что измеряет специфичность

Специфичность — это показатель браузера, определяющий степень точности выбора элемента с помощью селектора. Не все селекторы имеют одинаковый вес. Сравним селектор типа:

p {
  color: red;
}

селектор класса:

.text {
  color: blue;
}

и селектор id:

#title {
  color: green;
}

Если все три селектора соответствуют одному и тому же элементу, браузер ранжирует их согласно фиксированной иерархии типов селекторов, от самого сильного к самому слабому:

Inline styles
     ↓
IDs
     ↓
Classes / pseudo-classes / attributes
     ↓
Elements / pseudo-elements

Когда конкурирующие объявления имеют одинаковую важность, побеждает более специфичный селектор. В данном случае победит правило id, и текст будет зеленым.

Важное уточнение к верхнему ряду: стили, заданные непосредственно в элементе через атрибут style, не являются селекторами, и текущие спецификации рассматривают их как отдельный шаг, имеющий приоритет над любым заявлением автора, основанным на селекторе. Хотя их обычно моделируют как самую высокоприоритетную «колонку» специфичности, на практике это приводит к тем же результатам, поэтому в остальной части этого руководства сохраняется эта удобная модель.

Интерпретация специфичности как четырех колонок

Наиболее распространенное заблуждение заключается в том, что специфичность — это единый показатель, который можно сложить. Ее лучше рассматривать как кортеж из четырех значений, по одному на категорию:

Inline   |   IDs   |   Classes   |   Elements

Для заданного селектора подсчитывается, сколько его частей относятся к каждой категории. Самый простой случай — селектор класса:

.button {
  background: blue;
}

Он не содержит ни стилей, заданных непосредственно в элементе, ни идентификатора id, только один класс и без элементов:

Inline styles → 0
IDs           → 0
Classes       → 1
Elements      → 0

что можно кратко записать следующим образом:

0, 0, 1, 0

Теперь рассмотрим более сложный селектор:

nav#main .button div {
  background: green;
}

Он содержит один id (#main), один класс (.button) и два селектора типа (nav и div); следовательно, его специфичность составляет:

0, 1, 1, 2

Чтобы сравнить два селектора, браузер читает эти туплы слева направо, начиная с наиболее значимой колонки. Результат определяется первой колонкой, в которой они отличаются, а колонки справа уже не имеют значения. Именно поэтому один id превосходит любое количество классов, а один класс превосходит любое количество селекторов типа: значение из одной колонки не переносится в следующую, поэтому десять классов никогда не «суммируются» в один id. Вам не нужно запоминать арифметику; вам нужно помнить порядок сравнения.

Рассмотрение реального конфликта

Рассмотрим кнопку, у которой есть как класс, так и id. Обратите внимание, что это обычная маркировка HTML, поэтому используется class, а не className из JSX:

<button class="button" id="submit">
  Don't Click
</button>

Теперь предположим, что в таблице стилей содержится правило для класса:

.button {
  background: blue;
}

плюс несколько других правил, включая селектор типа, длинный селектор потомков и правило id-плюс-класс с состоянием hover:

button {
  background: purple;
}

nav#main .button div {
  background: green;
}

#submit.button:hover {
  background: yellow;
}

Все они задают свойства background, поэтому между ними происходит конкуренция. Браузер не просто берет то, что указано последним; сначала он сравнивает уровень специфичности.

В этом блоке есть деталь, которую легко упустить: nav#main .button div на самом деле относится к div, вложенному в элемент с классом button, а не к самой кнопке. Поскольку объектом селектора является его правейшая часть, это правило никогда не совпадает с нашим <button>, независимо от его специфичности. Проверка того, совпадает ли правило вообще, всегда является первым шагом при отладке.

Для правил, которые действительно совпадают, сравните селектор класса:

.button

с простым селектором типа:

button

В первом случае указан один класс, во втором — только один элемент. Следовательно:

.button

имеет приоритет выше:

button

и кнопка синяя, а не фиолетовая, хотя правило для фиолетового цвета появляется позже.

Добавьте id в схему:

#submit.button

Использование id ставит этот селектор в более высокий приоритет, чем любой селектор, состоящий только из классов и типов. При сравнении слева направо колонка id сразу определяет победителя, поэтому один id может превзойти селектор, состоящий из множества классов.

Почему правильное правило :hover может ничего не сделать

Этот случай вызывает много путаницы во время отладки. Начните с базового правила для кнопки:

#submit.button {
  background: red;
}

и с правилом для эффекта наведения на тот же элемент:

#submit.button:hover {
  background: yellow;
}

Правило для эффекта наведения содержит один id, один класс и одну псевдокласс, что придает ему более высокий приоритет, чем базовому правилу; поэтому при наведении кнопка становится желтой, как и ожидалось.

Теперь представьте, что другая часть кодовой базы форматирует ту же кнопку с помощью гораздо более длинного селектора:

nav#main div#container #submit.button {
  background: red;
}

в то время как правило для эффекта наведения остается неизменным:

#submit.button:hover {
  background: yellow;
}

Правило для эффекта наведения по-прежнему содержит псевдокласс:

:hover

Псевдоклассы действительно учитываются в столбце с классами. Однако это добавляет всего один балл на уровне класса. Длинный селектор содержит три идентификатора, в то время как правило для эффекта hover имеет только один, поэтому оно выигрывает в столбце с идентификаторами ещё до сравнения классов. В результате получается правило :hover, которое синтаксически безупречно, соответствует элементу, но при этом не изменяет ничего на экране.

Урок заключается в том, что когда состояния интеракции кажутся некорректными, причиной редко бывает псевдокласс. Обычно проблема в том, что какое-то другое заявление имеет более высокую специфичность. Инструменты разработчика браузера позволяют это увидеть: в панели Стили перечисляются все соответствующие правила, а те заявления, которые проиграли, выделяются подчёркиванием, что позволяет точно определить, какой селектор превосходит ваш.

В случае равенства специфичности решает порядок исходного кода

Иногда у двух селекторов одинаковая специфичность. Возьмём два правила с одинаковым селектором класса:

.button {
  background: red;
}
.button {
  background: blue;
}

Они одинаково специфичны и одинаково важны, поэтому ни первый, ни второй уровни не могут принять решение. Тогда браузер переходит к порядку указания: побеждает та декларация, которая появляется позже. С учетом этого порядка:

.button {
  background: red;
}
.button {
  background: blue;
}

кнопка в итоге становится синей.

Весь процесс принятия решения можно представить как последовательность правил для разрешения равенства:

Importance
    ↓
Specificity
    ↓
Source Order

Каждый уровень рассматривается только в том случае, если предыдущий не смог определить победителя. Сначала учитывается важность; если она одинакова, решает специфичность; если и это приводит к равенству, побеждает последняя декларация.

Цена использования !important

Почти каждый разработчик использовал этот выход из сложной ситуации хотя бы один раз:

color: red !important;

Добавление атрибута !important повышает приоритет объявления. Поскольку приоритет проверяется раньше специфичности, объявление с этим атрибутом может преобладать над объявлением с гораздо более высокой специфичностью. Например:

.button {
  background: purple !important;
}

такое объявление будет иметь преимущество перед обычным объявлением в случае длинного селектора, содержащего множество идентификаторов. Когда два объявления с атрибутом !important конкурируют, браузер снова сравнивает их специфичность, а затем порядок их расположения.

Именно эта возможность делает её рискованной. Типичная спираль отладки выглядит следующим образом:

"My style isn't working."
          ↓
"Let's increase the specificity."
          ↓
"Still not working."
          ↓
"Let's add !important."
          ↓
    "It works!"

Стиль наконец отображается, но основной конфликт не исчез; он переходит к следующему человеку, который должен переопределить этот свойство, и теперь ему приходится бороться со своим собственным !important. По мере накопления подобных случаев становится всё сложнее понимать стилишит. Используйте !important только в крайнем случае, а внезапная необходимость в его применении рассматривайте как сигнал о том, что CSS, вероятно, требует рефакторинга.

Прежде чем что-либо писать:

!important

задайте более полезный вопрос: почему мое объявление не имеет преимущества? Затем проверьте уровни в следующем порядке:

  • Является ли конкурирующее объявление более важным?
  • Является ли конкурирующий селектор более специфичным?
  • Появляется ли конкурирующее объявление позже в исходном коде?

Существуют законные случаи использования, такие как классы-утилиты, предназначенные для постоянного применения стилей, или переопределение встроенных стилей от стороннего виджета, которые невозможно изменить, однако их применение должно быть осмысленным, а не автоматическим.

Создание селекторов, которые естественным образом работают

Здесь также важен аспект обслуживаемости кода. Когда стиль не применяется, возникает соблазн делать селектор всё длиннее и длиннее, пока он не сработает:

body div section nav ul li a.button {
  color: red;
}

Этот метод работает, но каждая дополнительная часть селектора повышает сложность будущего переопределения стилей, связывает стиль с конкретной структурой DOM и затрудняет чтение таблицы стилей. Вместо того чтобы искать способы заставить селектор сработать любой ценой, следует задуматься о том, как организовать CSS так, чтобы предусмотренное объявление стиля сработало само по себе. Использование одного класса для большинства селекторов, избегание использования идентификаторов для стилизации и группировка более специфичных правил рядом с теми элементами, которые они модифицируют, всё это способствует улучшению обслуживаемости кода.

Это особенно важно в больших кодовых базах, где многие люди пишут CSS. Специфичность предназначена для того, чтобы сделать результаты предсказуемыми, а не для того, чтобы превратиться в гонку вооружений между селекторами.

Использование порядка загрузки с применением стилей от сторонних разработчиков

Порядок загрузки становится практическим инструментом, когда вы сочетаете собственные стили с файлами для сброса настроек или стилями от сторонних разработчиков. Эти файлы обычно задают стили для распространенных элементов, и вы хотите, чтобы ваши правила переопределяли их. Загрузка вашего стилевого файла после их делает это простым:

<link rel="stylesheet" href="reset.css">
<link rel="stylesheet" href="style.css">

Когда у ваших селекторов такая же специфичность, как у селекторов библиотеки, побеждает файл, загруженный позже; поэтому размещение style.css после reset.css позволяет вашим объявлениям вступить в силу без увеличения количества селекторов.

Зависимость от порядка тоже имеет свои издержки. Если кто-то позже изменит порядок тегов <link> или импортов в точке входа бандлера, стили могут незаметно измениться. Там, где это возможно, предпочитайте правила с такой специфичностью, чтобы победитель был ясен, и используйте порядок исходного кода в качестве окончательного критерия решения, для чего он и предназначен. Уровни каскадирования, если они поддерживаются целевыми браузерами, — это более явный способ указать, что «сначала библиотека, а затем наши стили».

После выбора победителя: каскадированное значение

На этом этапе на первый вопрос уже дан ответ. Браузер собрал все объявления свойства, оценил их значимость, затем специфичность, после чего — порядок исходного кода, и выбрал одно из них. Это победное значение называется каскадированным значением.

Однако работа ещё не полностью завершена. Предположим, что победным объявлением является:

width: 66%;

Такой процент нельзя отрисовать напрямую; браузер всё равно должен его обработать, определив его фактическую длину по отношению к контейнирующему блоку. Эта обработка значений вместе с наследованием свойств, которые вообще не объявлены, является следующим этапом преобразования CSS в пиксели.

Основные выводы

  • Каскадное применение правил разрешает конфликты в фиксированном порядке: сначала по степени важности, затем по специфичности, а потом по порядку источника.
  • Специфичность определяется сравнением четырех категорий (встроенные свойства, идентификаторы, классы/псевдоклассы/атрибуты, элементы/псевдоэлементы), причем сравнение происходит слева направо; значения из нижних категорий никогда не учитываются в вышележащих категориях.
  • Перед сравнением специфичности необходимо убедиться, что правило действительно соответствует элементу; самой важной частью селектора является та, на которую он направлен.
  • Правило с псевдоклассом :hover или другим подобным псевдоклассом добавляет вес только на уровне класса и может потерпеть поражение от более специфичного базового правила.
  • Атрибут !important дает преимущество путем изменения уровня важности, а не решения конфликта; используйте его осознанно и осторожно.
  • Преферируйте краткие селекторы, основанные на классах, и разумный порядок файлов стилей, чтобы желаемое правило применялось без усугубления конфликтов.