Как каскад выбирает победителя: важность, специфичность и порядок источников
Узнайте, как браузеры решают конфликты между заявлениями CSS, как интерпретировать специфичность через четырехэтапное сравнение, и почему правила для состояния hover и !important так часто ставят вас в тупик.
Когда несколько правил 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 дает преимущество путем изменения уровня важности, а не решения конфликта; используйте его осознанно и осторожно.