Когда два правила задают одному элементу разные значения, браузер решает спор по алгоритму каскада. Его нужно знать наизусть: «почему мой стиль не применился» спрашивают почти на каждом собесе по вёрстке.
Порядок каскада
Сравнение идёт по шагам, следующий шаг смотрят только при равенстве на предыдущем:
- Происхождение и важность: стили браузера, автора, пользователя;
!importantпереворачивает порядок. - Встроенные стили из атрибута
styleпобеждают любые обычные авторские правила из таблиц. - Каскадные слои (
@layer). - Специфичность селектора.
- Порядок появления: при равенстве побеждает правило ниже по коду.
Специфичность
Считается тройкой (A, B, C) и сравнивается слева направо, как номер версии:
- A: селекторы по id (
#menu). - B: классы (
.btn), атрибуты ([type="text"]), псевдоклассы (:hover,:nth-child()). - C: теги (
div) и псевдоэлементы (::before). - Ноль: универсальный селектор
*, комбинаторы (,>,+,~).
#nav .item a /* (1, 1, 1) */
.nav .item a:hover /* (0, 3, 1) */
ul li a /* (0, 0, 3) */
Первый побеждает второй, хотя у второго больше частей: один id весит больше любого числа классов. Никакого «перехода разряда» нет, 11 классов не превращаются в id.
:is, :where, :not, :has
:is(a, b)и:not(a, b)и:has(a, b)берут специфичность самого специфичного аргумента::is(#id, .cls)весит как id.:where(...)всегда даёт ноль. Им удобно писать базовые стили библиотек, которые легко переопределить.
:where(.card) h2 { margin: 0; } /* (0, 0, 1) */
.card h2 { margin: 0; } /* (0, 1, 1) */
!important и inline
!important поднимает объявление в отдельный, более сильный слой каскада. Встроенный style="..." сильнее любых обычных правил из таблиц, но проигрывает !important из таблицы. Злоупотребление !important ведёт к гонке «ещё один important сверху», поэтому его оставляют для утилит и переопределения сторонних стилей.
Каскадные слои
@layer reset, base, components, utilities;
@layer components { .btn { padding: 8px 16px; } }
@layer utilities { .p-0 { padding: 0; } }
- Порядок слоёв задаётся первым упоминанием, обычно одной строкой сверху.
- Более поздний слой побеждает более ранний независимо от специфичности:
.p-0перебьёт даже#app .btn, если тот лежит в слоеcomponents. - Стили вне слоёв сильнее любых слоёв.
- Для
!importantпорядок обратный: важное объявление из раннего слоя сильнее важного из позднего.
Слои решают вечную проблему «сброс и библиотека перебивают мои стили из-за специфичности». Поддерживаются всеми основными браузерами с 2022 года.
Наследование
Часть свойств наследуется по умолчанию: в основном текстовые (color, font-*, line-height, visibility, cursor). Блочные (margin, padding, border, width, background) не наследуются. Унаследованное значение слабее любого правила, которое нацелено на сам элемент, даже *.
Ключевые слова для любого свойства:
inheritвзять значение родителя;initialначальное значение из спецификации (дляdisplayэтоinline!);unsetвести себя какinheritдля наследуемых и какinitialдля остальных;revertоткатить к стилям браузера.
Полезные селекторы
li:nth-child(odd) /* нечётные */
li:nth-child(3n + 1) /* 1, 4, 7... */
p:first-of-type /* первый p среди соседей */
input:not([disabled])
a[href^="https"] /* начинается с */
a[href$=".pdf"] /* заканчивается на */
.list > li + li /* li, прямо перед которым стоит li (все, кроме первого) */
:focus-visible /* фокус с клавиатуры */
:has() - «родительский селектор»
.card:has(img) { padding-top: 0; } /* карточка с картинкой */
.field:has(input:invalid) label { color: red; }
h2:has(+ p) { margin-bottom: 4px; } /* h2, за которым сразу идёт p */
:has() выбирает элемент по тому, что внутри или после него. Раньше это требовало JavaScript. Поддерживается всеми основными браузерами с конца 2023 года.
Нативный nesting
.card {
padding: 16px;
& h2 { font-size: 20px; }
&:hover { box-shadow: 0 2px 8px #0002; }
.dark & { background: #222; }
}
& обозначает родительский селектор. Специфичность вложенного правила считается так, будто родитель обёрнут в :is(). Поддерживается во всех основных браузерах.
Частые ошибки
- Считать специфичность одним числом «100 + 10 + 1».
- Чинить конфликт через
!importantвместо понижения специфичности. - Думать, что
:where()весит как его аргумент. - Ждать, что
.parent { margin: 0 }унаследуется детьми.
Что спросят на собесе
- Как считается специфичность? Посчитай для конкретного селектора.
- Кто победит: inline-стиль или
!important? - Чем
:is()отличается от:where()? - Что дают каскадные слои?
- Что такое
:has()и пример его применения.