Как работают каскад и наследование в CSS?

MiddleCSS #css #cascade

Вопрос

Как CSS решает, какое из конфликтующих правил применить к элементу, и как работает наследование свойств от родителя к потомку?

Короткий ответ

Каскад — это алгоритм, который при нескольких применимых правилах к одному элементу выбирает итоговое значение по цепочке критериев: происхождение и важность правила (!important, автор/браузер/пользователь), специфичность селектора и порядок объявления в коде. Наследование — отдельный механизм: часть свойств (в основном текстовые) по умолчанию передаётся от родителя к потомку, если явно не переопределена.

Подробный ответ

Каскад разрешает конфликт между несколькими правилами, которые применяются к одному элементу, в следующем порядке приоритета (от менее важного к более важному):

  1. Origin (происхождение) и важность — стили браузера по умолчанию (user agent) → стили пользователя → стили автора (наши) → стили автора с !important → стили пользователя с !important. На практике чаще всего сравниваются "обычные" авторские правила и !important-правила: !important переворачивает порядок важности и должен использоваться очень осторожно, так как ломает предсказуемость каскада.
  2. Specificity (специфичность) — считается по селектору: инлайн-стиль > id > класс/атрибут/псевдокласс > тег. Более специфичный селектор побеждает независимо от порядка в файле. (Подробный расчёт specificity — отдельная тема.)
  3. Source order (порядок в коде) — при равной специфичности и важности побеждает правило, объявленное позже в каскаде (позже в файле, либо в файле, подключённом позже).

Наследование — независимый от каскада механизм: значение свойства, не заданное явно для элемента, может браться от родителя. Не все свойства наследуются:

  • Наследуются по умолчанию — в основном текстовые/типографские свойства: color, font-family, font-size, line-height, text-align, visibility, cursor.
  • Не наследуются по умолчанию — в основном свойства блочной модели и позиционирования: margin, padding, border, width, height, background, position, display. Логика в том, что такие свойства специфичны для конкретного блока и наследование их сделало бы вёрстку непредсказуемой.

Управляющие ключевые слова, применимые к любому свойству:

  • inherit — принудительно взять значение родителя, даже если свойство обычно не наследуется.
  • initial — сбросить к значению по умолчанию из спецификации CSS (не браузера!), например display: initial вернёт значение по умолчанию для этого элемента согласно спеке.
  • unset — ведёт себя как inherit для наследуемых свойств и как initial для ненаследуемых — универсальный "сброс к естественному поведению".
  • revert — откатывает к стилю предыдущего слоя каскада (обычно к стилю браузера по умолчанию), в отличие от initial, который откатывает к спецификации CSS, а не к тому, что реально показал бы браузер без стилей автора.

Пример

/* наследование: color и font-family достаются потомкам без явного указания */
body {
  color: #222;
  font-family: sans-serif;
}

.card {
  /* border, padding, background — НЕ наследуются, их нужно задавать явно */
  border: 1px solid #ddd;
  padding: 16px;
}

.card p {
  /* explicit inherit: принудительно взять цвет ссылки от родителя */
  color: inherit;
}

/* каскад по специфичности: .warning (класс) сильнее p (тег) */
p {
  color: black;
}
.warning {
  color: crimson; /* победит, т.к. специфичность класса выше тега */
}

/* !important переворачивает обычный порядок каскада — использовать осторожно */
.legacy-override {
  color: blue !important;
}

/* сброс к состоянию "как будто автор ничего не писал" */
.reset-all {
  all: unset;
}

Дополнительные вопросы

  • Чем all: unset отличается от all: revert?
  • Как каскадные слои (@layer) меняют порядок разрешения конфликтов между командами в большом проекте?
  • Почему !important считается антипаттерном и как без него решить конфликт специфичности?