Правильная шпаргалка по 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/новых Blink) | — | |
Авторские стили с !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
— тоже.
Но вот куда «воткнуть» эти мгновенные значения в каскад — пока вопрос. Стандарт-то однозначен: после всех обычных стилей (и браузерных, и авторских, и пользовательских), но перед всеми важными. Но на практике до марта 2020 года это было так лишь в Firefox. Поэтому и результат нашего примера в нем и других браузерах различался. Старое поведение Chrome было признано багом, пофиксили его лишь в 83-й версии.
После стилей анимаций (по стандарту) идут важные авторские стили. Они сортируются по тем же правилам, что обычные — сначала по специфичности, затем по порядку в коде, стили из атрибута style «бьют» все остальные.
В нашем примере на этом уровне «притаился» еще один интересный момент — объявление color: inherit !important
из селектора по атрибуту. Не путайте его с «автоматическим» наследованием, которое бывает, когда своих стилей у элемента нет (о котором было чуть выше). Ключевое слово inherit — такое же явное значение, как любое другое. Только не константа, а что-то вроде «локальной переменной», в которую подставляется вычисленное значение из родителя. Причем и для наследуемых свойств, и для ненаследуемых. И приоритет этого объявления подчиняется общим правилам. Как и для остальных универсальных значений (initial
, unset
и revert
).
Стиль с !important
в атрибуте style
— это предел приоритетности для авторских стилей. Поэтому в стандартных браузерах (на момент написания статьи это был один лишь Firefox, в марте 2020 к нему добавился Chrome 83+ c аналогами) значение оттуда и будет окончательным победителем в каскаде (что мы и видим). Перекрыть его могут лишь пользовательские стили с !important
(если есть) и браузерные стили с !important
(которых для цвета <body>
просто по логике быть не может:). Увы, остальные браузеры стандарт не соблюдают, и в них значения из анимации тоже перекрывают важные авторские стили. Учитывайте это, особенно когда возникает соблазн «быстренько подфиксить» что-нибудь на динамичной странице, добавив !important
.
И наконец, «король» каскада, даже «джокер», перекрывающий вообще всё везде — это мгновенное значение свойства во время действия transition
. Но как у всякого супермогущества, у него есть существенное ограничение: оно не может переопределить начальное значение в статике (по крайней мере, я такого способа, тем более кроссбраузерного, не знаю — если вы знаете, добро пожаловать в комментарии!), а может лишь предотвратить внезапное изменение в динамике. Когда для CSS-свойства задан transition
, то даже при мгновенной смене указанного значения (например, при срабатывании псевдокласса типа :hover
, либо скриптом) каскадное значение (а вместе с ним и фактическое) будет меняться не резко, а постепенно, с заданными длительностью и плавностью. Это изменение можно даже отложить с помощью transition-delay
— в том числе очень надолго. На такой «почти бесконечной» задержке основан один из известных хаков для имитации состояния в CSS (например, после клика).
Пример для этой статьи я решил transition
-ом не усложнять. Но у нас про него есть отдельная статья. Хотя ей уже три года, и в спецификациях с тех пор многое поменялось, эта главная особенность по-прежнему в силе. Так что вооружайтесь знанием и смело повелевайте стихией CSS-каскада! И на всякий случай не забывайте следить за обновлениями стандартов (ну и нашего сайта, конечно:).
P.S. Это тоже может быть интересно:
В мобильной версии таблица выглядит не как таблица, а как текст с произвольным выравнивание по горизонтали
Спасибо за багрепорт! В ближайшее время исправим (там механизм адаптивности споткнулся на объединенных ячейках).
Сложно написано. Я ни хрена не понял.
Претензия принята. Постараюсь упростить!
Скажите, Пользовательские и Авторские стили — как вы их разделяете?
Мне кажется в таблице что-то пошло не так.
В теории предполагалось, что пользователи смогут добавлять в браузеры свои CSS-файлы, которые переопределяли бы браузерные дефолты (скажем, более крупный/контрастный шрифт и т.п.). На практике это толком не прижилось (была попытка в IE, но про интерфейс для этого дела как-то не подумали, а писать CSS руками обычным пользователям неудобно). Так что (практически) весь CSS, который мы пишем и подключаем к страницам, неважно как именно — это авторские стили. И строчку с пользовательскими стилями в таблице я оставил пустой, потому что в стандарте они есть, но реально (почти) не встречаются.
Признаю, что получилось не очень наглядно. Постараюсь исправить!
Вы забыли одну важную вещь — дело в том, что в наследуемых значениях тоже есть свой каскад, и насколько я могу понять, не знаю, есть ли это в спецификации — приоритет как с обычными свойствами — побеждают наследуемые от авторских стилей, потом от пользовательских, потом от браузерных.
Ради любопытства откопал на диске портативные версии старых хромов. В хроме 77 текст всё ещё фиолетовый, а в 83 уже серый. Видимо, где-то в этом промежутке они всё-таки поправили каскад
Спасибо! Действительно, как раз в 83-й версии тот древний баг пофиксили, а я забыл обновить статью. Но сейчас исправил!