Урок по треку HTML и CSS 8 мин

Селекторы, специфичность и каскад за 8 минут

Каскад, специфичность, наследование, :is, :where, :has и каскадные слои.

Когда два правила задают одному элементу разные значения, браузер решает спор по алгоритму каскада. Его нужно знать наизусть: «почему мой стиль не применился» спрашивают почти на каждом собесе по вёрстке.

Порядок каскада

Сравнение идёт по шагам, следующий шаг смотрят только при равенстве на предыдущем:

  1. Происхождение и важность: стили браузера, автора, пользователя; !important переворачивает порядок.
  2. Встроенные стили из атрибута style побеждают любые обычные авторские правила из таблиц.
  3. Каскадные слои (@layer).
  4. Специфичность селектора.
  5. Порядок появления: при равенстве побеждает правило ниже по коду.

Специфичность

Считается тройкой (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() и пример его применения.

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.