Правильная шпаргалка по CSS-каскаду

Написать эту статью меня подтолкнула относительно недавняя статья на CSS-tricks (скорее всего, вы ее уже видели, ссылку не дам из вредности:). Ее автор проделал большую и замечательную работу — нарисовал красивую наглядную схему-«шпаргалку», написал объяснение простым языком, привел кучу примеров, не забыл даже про презентационные атрибуты, тоже влияющие на стили (в SVG)… Увы, даже та статья подтвердила два печальных правила: 1) никто не знает CSS, 2) никто не читает спецификаций. Так что первая ее редакция транслировала одно из популярных заблуждений о каскаде. К чести автора, он оперативно исправил и схему, и статью — но если бы он заглянул в стандарт, этого могло бы и не понадобиться…

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

Как мы уже знаем, для каждого свойства каждого элемента CSS-каскад собирает все правила с упоминанием этого свойства, применяемые к этому элементу. А затем сортирует их по определенным критериям. Для сегодняшних практических задач нам хватит трех критериев, перечисленных в модуле каскада и наследования предпоследнего, 3 уровня: сперва по происхождению и важности, потом по специфичности и, наконец, по порядку в коде. В более новых спецификациях упоминаются и другие критерии, но до них дойдем тогда, когда они начнут реально на что-то влиять.

Помимо этого, в получении вычисленного значения CSS-свойства может участвовать значение, унаследованное от родителя (для наследуемых свойств). И да, иногда итоговые стили элемента зависят от других особенностей элемента — чаще всего атрибутов. Нагляднее всего это в SVG, где эти презентационные атрибуты (stroke, fill, r и т.д.) буквально соответствуют одноименным CSS-свойствам. По актуальному стандарту SVG2 даже d у <path> соответствует CSS-свойству, что позволяет анимировать стилями сами контуры SVG-фигур (правда, пока лишь в «хромятах»). Но подобные атрибуты есть и в HTML. С некоторыми из них, вроде width и height для <img>, мы по сей день регулярно встречаемся. Другие, вроде text и bgcolor у <body> (задающие ему цвет текста и фона соответственно) почти забыты и встречаются лишь на реликтовых страницах из 90-х — но браузеры их поддерживают, поскольку таких страниц еще немало. И еще у каждого свойства есть начальное значение, которое назначается ему, если для этого элемента нигде ничего больше не указано (на «турнир» каскада никто не явился и он не состоялся:).

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

Источник Специфичность Возможное количество значений
Начальное значение Всегда одно
Унаследованное значение (для наследуемых свойств) Максимум одно
Браузерные стили без !important 0, 0, 1 Сколько угодно
0, 0, 2 Сколько угодно
и т. д.
Пользовательские стили без !important
Авторские стили без !important

(CSS-in-JS тоже попадают сюда!)

Из презентационных атрибутов HTML и SVG 0, 0, 0 Максимум одно
Из <style> и <link> 0, 0, 0 Сколько угодно
0, 0, 1 Сколько угодно
и т. д.
Из атрибута style="…" Сколько угодно
Стили при анимации (по стандарту и в Firefox) Максимум одно
Авторские стили с !important Из <style> и <link> 0, 0, 0 Сколько угодно
0, 0, 1 Сколько угодно
и т.д.
Из атрибута style="…" Сколько угодно
Пользовательские стили с !important
Браузерные стили с !important 0, 0, 1 Сколько угодно
0, 0, 2 Сколько угодно
и т.д
Стили при анимации (в WebKit/Blink/Edge, не по стандарту) Максимум одно
Стили во время перехода (если есть) Максимум одно

Таблица делится на крупные «ярусы» (левая колонка). На некоторых ярусах может быть максимум одно значение: свойство родительского элемента либо наследуется, либо нет, свойство либо анимируется, либо нет, и т.д. Другие ярусы подразделяются на «подуровни» — например, элементу может соответствовать много разных селекторов, как в наших обычных стилях (стандарт называет их авторскими), так и во встроенных стилях браузера. Значения, приходящие из этих селекторов, сначала сортируются по специфичности. А при равной специфичности — просто по порядку.

Применится то значение, которое попадёт в эту таблицу ниже всех.

Давайте быстренько применим эту шпаргалку на практике. Я заготовил специальный пример на Codepen, который пытается задать цвет для body сразу несколькими способами:

See the Pen Пример действия каскада by Ilya Streltsyn (@SelenIT) on CodePen.

В нем нет «хитрых» селекторов, где надо кропотливо подсчитывать циферки специфичности: главная хитрость далеко не в ней. Зато свойство color — наследуемое. И для элемента body, который мы сегодня «препарируем», существует исторический HTML-атрибут text, тоже влияющий на цвет текста.

Очередность применения значений к свойству для элемента нагляно (пояснение в тексте)

Все значения для свойства color, которые в принципе могли бы примениться к нашему элементу <body>, собраны в правой части рисунка — в порядке их появления в коде. А слева видно, на каком «ярусе» таблицы это значение в итоге окажется. Чтобы рассмотреть получше, можно открыть более крупную версию картинки по клику.

Пробежимся по этому списку и отметим интересные моменты.

У начального значения, очевидно, в таком раскладе шансов нет: оно может примениться, лишь если больше нет ничего другого, а у нас тут целая куча всего.

С унаследованным значением (от родительского элемента <html>, он же :root) ситуация похожая: оно применилось бы, если бы самому body не было задано никаких стилей. Как это значение попало к самому родителю — абсолютно неважно: наследуется итоговое, вычисленное значение (подробнее о «жизненных стадиях» CSS-значения — в предыдущей нашей статье про каскад). Так что !important в стилях для html никак не влияет на приоритетность родительского стиля для body. Ну а если бы у нас было не свойство color, а какое-нибудь ненаследуемое свойство, типа width или display — мы бы и вовсе пропустили этот этап целиком.

Далее идут стили, «зашитые» в коде самого браузера. Для свойства color у body, насколько я могу судить, в браузерных стилях (ни для WebKit, ни для Gecko) никакого значения не задано. Дефолтный цвет там задается корневому элементу и наследуется от него по всему дереву. Так что на этом этапе у нас никаких изменений. И на этапе пользовательских стилей — тоже: их просто нет (скорее всего). Вы вообще давно видели пользовательские стили?..

Больше всего разноцветных стрелок переплелось на этапе авторских стилей. Неспроста: этот этап для нас самый важный. Настолько, что многие учебники и статьи по CSS другие этапы и не рассматривают (а зря;). Сюда попадает львиная доля стилей, которые задаем элементам мы, веб-разработчики — неважно, по старинке, через CSS-файлы или тег <style>, или по-модному, «-in-JS» (на выходе которого всё равно будет что-то из них). И любые методологии и инструменты избавляют лишь (в лучшем случае) от путаницы со специфичностью, но никак не «от каскада» вообще:).

Все значения каждого свойства из авторских стилей, в принципе применимые к нашему элементу, собираются в кучу и сортируются сперва по специфичности селектора, затем по порядку подключения. Способ подключения, вопреки расхожему мифу, вообще ни на что не влияет. Разве что @import может подключаться только в самом начале и из-за этого проигрывает последующим стилям, но причина — именно порядок объявления, а не какая-то «особость» @import.

Первая отдельная строчка в блоке «Авторские стили», со значением red, может удивить: как связаны CSS-свойство color и HTML-атрибут с совсем другим именем? В этом замешана древняя браузерная магия под названием «подсказки для представления» (presentationl hints). Как ни странно, она четко прописана в стандартах. Для каждого языка стандарт описывает, какие атрибуты соответствуют какому свойству, по каким правилам преобразуются их значения (например, те же width/height для <img> с числовыми значениями переводятся в одноименные CSS-свойства с дописыванием 'px'), и место этих добавочных стилей в каскаде. Для HTML и SVG это место — в самом начале авторских стилей с нулевой специфичностью, перед всеми прочими (правда, в SVG2 с ними вышел забавный казус, ведь никто не хочет учить CSS-каскад:). Для простоты можно считать, что у презентационных атрибутов специфичность ниже нуля (как предлагает Амелия Беллами-Ройдз).

У универсальных селекторов (*), в любых сочетаниях с любыми комбинаторами (пробел, >, ~, +), специфичность тоже равна нулю. Если бы в стилях для * > * («любой элемент, у которого есть родитель», т.е. все кроме корневого) не было !important, первой строчкой в блоке «авторские стили из <style> и <link>» была бы строчка со специфичностью 0, 0, 0 и значением yellow. Но всё равно она шла бы после стилей из презентационных атрибутов.

Далее, селектор по тегу (специфичность 0, 0, 1) проигрывает селекторам по классу и псевдоклассу (у обоих специфичность 0, 1, 0, ведь :not() сам «не считается»), и из них побеждает последний.

У нашего <body> есть еще и атрибут style, в котором color задан аж дважды. Это нормально: в любом CSS-правиле может быть сколько угодно объявлений одного и того же свойства, и при одинаковой важности побеждает последнее, которое браузер понял — на этом строится возможность «фолбэчного» поведения, залог устойчивости CSS. В нашем случае и важность разная, поэтому объявления «разлетаются» по разным ярусам таблицы: обычное объявление побеждает стили из <style> и <link>, но при наличии важного объявления это уже неважно (простите за каламбур:).

А теперь — самое интересное, о чем молчат (или врут:) почти все статьи о CSS-каскаде: анимация.

К нашему <body> применена анимация, меняющая значение color. И даже не одна, а целых две! Это тоже нормально: свойство animation допускает множественные значения, так что несколько анимаций вполне могут применяться к одному элементу одновременно. Если эти анимации затрагивают одно и то же свойство (как у нас), единственное итоговое значение берется из последней анимации в перечислении (в нашем случае — анимации с именем purple). Как и с родительским значением при наследовании, абсолютно неважно, откуда и как, с !important или без, нашему элементу досталось значение свойства animation: при расчете цвета нам важно лишь то, есть ли вообще у элемента анимация, затрагивающая свойство color. Анимации через @keyframes и через Web Animations API (где он поддерживается) учитываются одинаково.

В нашем примере значение в анимации фактически не меняется (начальное и конечное значения совпадают), но в общем случае надо учитывать, что речь идет о «мгновенном» значении свойства, которое пересчитывается для каждого кадра анимации. При transition — тоже.

Но вот куда «воткнуть» эти мгновенные значения в каскад — пока вопрос. Стандарт-то однозначен: после всех обычных стилей (и браузерных, и авторских, и пользовательских), но перед всеми важными. Но на практике это так лишь в Firefox. Поэтому и результат нашего примера в нем и других браузерах различается. Поведение Chrome признано багом, поэтому менять стандарт Рабочая группа CSS пока не готова, но вот что-то фиксить этот баг уже минимум три года никто не торопится…

После стилей анимаций (по стандарту) идут важные авторские стили. Они сортируются по тем же правилам, что обычные — сначала по специфичности, затем по порядку в коде, стили из атрибута style «бьют» все остальные.

В нашем примере на этом уровне «притаился» еще один интересный момент — объявление color: inherit !important из селектора по атрибуту. Не путайте его с «автоматическим» наследованием, которое бывает, когда своих стилей у элемента нет (о котором было чуть выше). Ключевое слово inherit — такое же явное значение, как любое другое. Только не константа, а что-то вроде «локальной переменной», в которую подставляется вычисленное значение из родителя. Причем и для наследуемых свойств, и для ненаследуемых. И приоритет этого объявления подчиняется общим правилам. Как и для остальных универсальных значений (initial, unset и revert).

Стиль с !important в атрибуте style — это предел приоритетности для авторских стилей. Поэтому в стандартных браузерах (на сегодня это один лишь Firefox:) значение оттуда и будет окончательным победителем в каскаде (что мы и видим). Перекрыть его могут лишь пользовательские стили с !important (если есть) и браузерные стили с !important (которых для цвета <body> просто по логике быть не может:). Увы, остальные браузеры стандарт не соблюдают, и в них значения из анимации тоже перекрывают важные авторские стили. Учитывайте это, особенно когда возникает соблазн «быстренько подфиксить» что-нибудь на динамичной странице, добавив !important.

И наконец, «король» каскада, даже «джокер», перекрывающий вообще всё везде — это мгновенное значение свойства во время действия transition. Но как у всякого супермогущества, у него есть существенное ограничение: оно не может переопределить начальное значение в статике (по крайней мере, я такого способа, тем более кроссбраузерного, не знаю — если вы знаете, добро пожаловать в комментарии!), а может лишь предотвратить внезапное изменение в динамике. Когда для CSS-свойства задан transition, то даже при мгновенной смене указанного значения (например, при срабатывании псевдокласса типа :hover, либо скриптом) каскадное значение (а вместе с ним и фактическое) будет меняться не резко, а постепенно, с заданными длительностью и плавностью. Это изменение можно даже отложить с помощью transition-delay — в том числе очень надолго. На такой «почти бесконечной» задержке основан один из известных хаков для имитации состояния в CSS (например, после клика).

Пример для этой статьи я решил transition-ом не усложнять. Но у нас про него есть отдельная статья. Хотя ей уже три года, и в спецификациях с тех пор многое поменялось, эта главная особенность по-прежнему в силе. Так что вооружайтесь знанием и смело повелевайте стихией CSS-каскада! И на всякий случай не забывайте следить за обновлениями стандартов (ну и нашего сайта, конечно:).

P.S. Это тоже может быть интересно: