Смотрим не исходник, а вычисленные стили
Первое, что нужно сделать, когда стиль не применяется, — открыть инспектор и перейти на вкладку «Вычисленные». Там перечислены все свойства элемента в алфавитном порядке, а не в том, в каком они написаны в коде. В этом списке четыре возможных состояния для каждого объявления:
- Объявления нет вовсе — правило не попало в каскад.
- Объявление есть, но написанного значения не дало — отброшено при разборе или позже, на вычислении значения, уже после каскада.
- Объявление есть, зачёркнуто и без предупреждения — проиграло в каскаде.
- Объявление есть и победило, но на элементе ничего не изменилось — дело уже не в том, какое объявление выиграло. Причин здесь несколько, и признак в инспекторе у каждой свой.
Каждый из этих исходов требует своего разбора.
Правила нет в списке
Если объявления нет в вычисленных стилях, значит, правило не попало в каскад. Причин может быть несколько:
Селектор не совпал
Опечатка в имени класса, неверное состояние элемента (например, :hover вместо :active), не тот узел в дереве (потомок вместо дочернего элемента). Проверяйте не только сам селектор, но и его окружение: если элемент находится внутри теневого дерева, авторские стили страницы до него не доходят.
Условие не выполнено
Медиа-запрос (@media), контейнерный запрос (@container) или @supports не сработали. Например, правило внутри @media (min-width: 1000px) не применится, если ширина экрана меньше. Проверяйте не только синтаксис, но и текущие условия.
Негодный селектор в списке
Один недопустимый селектор в списке через запятую отменяет всё правило целиком. Например:
.button, .link:hover, ::unknown-pseudo {
color: red;
}
Здесь ::unknown-pseudo — неизвестный псевдоэлемент, разобрать его нельзя. Из-за него стили не получат ни .button, ни .link:hover, хотя эти два селектора написаны верно. Несовпадение — не то же самое: селектор, под который в документе не нашлось ни одного элемента, остаётся допустимым и остальную часть списка не трогает. Внутри :is() и :where() список прощающий — негодные селекторы отбрасываются поштучно, но :has() прощающим не является.
Импорт ниже правил
Правило @import действует только в начале таблицы стилей: до него допустимы объявление кодировки, объявления слоёв и другие @import. Написанное ниже любого другого правила, оно отбрасывается целиком, и подключаемый файл не загружается вовсе. Симптом — стили библиотеки не применяются нигде.
Объявление есть, но написанного значения не дало
Если объявление присутствует в стилях элемента, но на элемент не влияет, значит, оно было отброшено на одном из этапов — и этапы различаются по виду в инспекторе: отброшенное при разборе зачёркнуто и помечено предупреждением, а отброшенное на вычислении значения показано победившим, без пометок.
Недопустимое значение или неизвестное свойство
Объявление с недопустимым значением или неизвестным свойством отбрасывается при разборе и в каскаде не участвует. Например:
.element {
color: invalid-value;
unknown-property: 10px;
}
Оба объявления будут проигнорированы.
Объявление с var()
Объявление с var() разбирается как допустимое, участвует в каскаде, но признаётся недопустимым позже, при вычислении значения. Результат равен unset:
- для наследуемых свойств — значение родителя;
- для ненаследуемых — начальное значение свойства.
В инспекторе такое объявление показано победившим и не зачёркнутым, но вычисленное значение не соответствует написанному. Это сбивает диагностику: внешне оно неотличимо от идущего перехода и активной анимации — там объявление тоже не зачёркнуто, а вычисленное значение написанному не соответствует. Развести их можно только по тому, идёт ли сейчас переход или анимация.
Объявление зачёркнуто
Если объявление зачёркнуто, значит, был спор, и решает его не только специфичность. Шаги сравнения идут в таком порядке:
-
Важность и происхождение
- Важные объявления (
!important) сравниваются отдельно от обычных. - Пользовательские важные объявления перебивают авторские.
- Важное объявление в атрибуте
styleперебивает важное объявление из таблицы стилей.
- Важные объявления (
-
Объявление из атрибута
styleЭто отдельный шаг каскада, который сравнивается раньше слоёв. Специфичность его не касается: оно выигрывает даже у правила из последнего слоя. -
Слои каскада
- Обычные объявления из более позднего слоя выигрывают независимо от селекторов.
- Обычные объявления вне слоёв считаются последним слоем и выигрывают у всех слоёв; у важных объявлений порядок слоёв обратный, и там объявление вне слоёв слабее объявления из любого слоя.
- Порядок слоёв задаётся первым упоминанием имени слоя, а не местом, где написаны правила.
-
Специфичность Сравнение идёт покомпонентно слева направо, и никакое количество классов не перевешивает один идентификатор. Как считается сама специфичность — в следующем разделе.
-
Порядок в готовом документе При полном равенстве выигрывает объявление, идущее позже. Готовый документ — это порядок узлов стилей в нём, а не порядок импортов в коде: сборщик переупорядочивает и склеивает файлы, генерация стилей из кода вставляет их во время выполнения, отложенно загруженный кусок приложения добавляет свои стили последним. Поэтому и проверять надо документ, а не строки в сборке.
Специфичность: как считать и где она вырастает незаметно
Специфичность записывается тройкой через дефис: 0-1-2. Считается она так:
- Идентификатор (
#id) добавляет единицу к первому компоненту. - Класс (
.class), атрибут ([attr]) или псевдокласс (:hover) — к среднему. - Тип элемента (
div) или псевдоэлемент (::before) — к третьему.
Что не повышает специфичность:
- псевдокласс
:where()— его специфичность всегда нулевая, что бы ни стояло в скобках; - универсальный селектор (
*); - комбинаторы (
>,+,~); - медиа-запросы, контейнерные запросы,
@supports.
Где специфичность вырастает незаметно:
:is(),:not(),:has()— берут специфичность самого специфичного аргумента внутри скобок, а сами по себе в подсчёт не входят; о самих этих селекторах — в материале Что современный CSS убрал из вашего JS.- Вложенный CSS — амперсанд (
&) считается как:is(): вложенное правило весит по наибольшей специфичности родительского списка селекторов. :nth-child(An+B of S)— добавляет специфичность селектораS.
Объявление победило, а эффекта нет
Если объявление написано, а на элементе ничего не изменилось, общего признака у этой ветки нет: каждая причина опознаётся по-своему — где-то по виду в инспекторе, где-то по самому симптому. Вот возможные варианты:
Свойство неприменимо к элементу
Например, width и height не действуют на строчные незаменяемые элементы, а свойства раскладки — на потомков элемента, который соответствующим контейнером не является; как устроен этот выбор, разобрано в материале Flexbox или Grid. В инспекторе объявление не зачёркнуто, а вычисленное значение совпадает с написанным — этим случай отличается от перехода, анимации и объявления с var(), где вычисленное значение написанному не соответствует. Признак этот не единственный: если свойство к элементу применимо, а эффекта всё равно нет, смотрите порядок отрисовки.
Идущий переход или активная анимация
Активная анимация перебивает обычное авторское объявление любой специфичности, а идущий переход перебивает всё, включая важные объявления. В инспекторе объявление не зачёркнуто, но вычисленное значение не соответствует написанному.
Сокращённое свойство переписало отдельное
Сокращённое свойство задаёт все свои составляющие, включая неназванные. Например:
.element {
margin-top: 20px;
margin: 10px;
}
Здесь верхний отступ задаст сокращённая запись: она переписала все составляющие, включая ту, что была назначена отдельно выше. Спор идёт по каждому объявлению отдельно, на уровне составляющих, и проигравшее отдельное объявление в инспекторе зачёркнуто.
Несостоявшееся наследование
Наследуются не все свойства. Например, рамка, внутренние отступы, фон и ширина не наследуются. Элементы форм по умолчанию не берут шрифт от родителя, пока он не задан им явно. В инспекторе объявления на элементе нет вовсе.
Порядок отрисовки
Если речь идёт о z-index, каскад тут ни при чём: объявление побеждает, а элемент всё равно внизу — дело в порядке отрисовки, а не в том, какое объявление выиграло. Этот случай разобран в материале z-index растёт, а элемент всё равно внизу.
Тупик: !important
Первым делом дописывают !important. Значение появляется, вопрос закрыт. Но через время это же правило нужно переопределить, и сверху ложится второй !important, потом третий. Отменить такое объявление обычным селектором уже нельзя.
Ловушка в том, что помогает !important не всегда, а цену берёт всегда:
- Если выиграло более специфичное объявление, значение появится, но настоящая причина останется неразобранной.
- Если объявление вообще не участвовало в каскаде (селектор не совпал, правило убито негодным селектором в списке),
!importantне меняет ничего.
Цена у !important всегда высокая: он переносит объявление в отдельную группу сравнения, где порядок происхождений перевёрнут. Важное пользовательское объявление перебивает важное авторское, а порядок слоёв для важных объявлений обратный.
Позиция редакции
Специфичность держать плоской: правило на уровне одного класса как норма, идентификаторы и вложенные утяжеления как исключение. Различия выражать порядком объявлений и слоями, а переопределять через revert-layer и revert, а не через селектор тяжелее предыдущего.
Такой подход работает, если вы контролируете весь код. Если же вы пишете поверх чужой библиотеки компонентов, которую нельзя изменить, и она уже расставила идентификаторы или инлайновые стили, понизить их специфичность вы не можете. В этом случае точечное важное объявление честнее и дешевле, но класть его надо в слой:
- если библиотека разложена по слоям — в слой, объявленный раньше её слоя;
- если библиотека отдана без слоёв — в любой слой, потому что важное объявление вне слоёв слабее важного объявления из любого слоя.
Граница: важное объявление в атрибуте style не перебивается из таблиц стилей ничем, и слои тут не помогут.