Главная / Статьи

Откуда берутся значения CSS при установке None: как работает наследование

Узнайте, как браузеры определяют, наследуется ли свойство, почему дочерние элементы получают вычисленное значение родителя, и как методы inherit и initial позволяют осуществлять явный контроль.

Издатель: REACTAPP.TOP10 мин чтения2243 слов

Источники: How Inheritance Works in CSS Internally at Algorithmic Level.

Вы устанавливаете свойство color для обёртки, и абзац внутри становится красным, хотя ни одно правило этого не предусматривало. В то же время отступы, заданные для той же обёртки, остаются на прежнем месте. Оба этих поведения обусловлены наследованием в CSS — механизмом, при котором задаётся значение, если у элемента его собственного нет. В этом руководстве объясняется, какие свойства наследуются, что именно передаётся дальше (ответ удивляет многих, когда речь идёт о процентах), как ключевые слова inherit и initial изменяют стандартные значения, и какое место занимает наследование среди других этапов, превращающих стильовую таблицу в окончательные значения.

Значение, которое никто не объявлял

Начнём с правила для родительского элемента:

.parent {
  color: red;
}

и с маркировкой, в которой абзац находится внутри этого родительского элемента:

<div class="parent">
  <p>Hello World</p>
</div>

Никакие правила не задают значение color для тега <p>, однако текст отображается красным. К моменту, когда браузер достигает этой части кода, он уже обработал все конфликтующие объявления с помощью механизма каскадирования и определил их значения. Остаётся лишь решить для каждого свойства каждого элемента, должно ли значение браться из собственных объявлений элемента или из более высоко расположенных в иерархии элементов. Наследование — это механизм, который помогает принимать такие решения.

Какие значения передаются по иерархии

Наследование позволяет определённым значениям свойств передаваться от родительского элемента к его дочерним элементам, а затем от них — к их дочерним элементам:

Parent
  ↓
Child
  ↓
Grandchild

Не все свойства участвуют в процессе наследования. Свойства, отвечающие за оформление текста, являются классическими примерами свойств, наследуемых от родительских элементов, такими как:

font-family
font-size
color

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

body {
  color: #333;
}
h1 {
  color: #333;
}
p {
  color: #333;
}
li {
  color: #333;
}

их можно задать один раз, высоко в иерархии документа, и позволить механизму наследования передавать эти значения дальше. В результате используется меньше CSS, и изменения дизайна нужно вносить только в одном месте. Установка базового шрифта и цвета для body или :root — это повседневное применение этой идеи.

Решение браузера для каждого свойства

Логика становится понятнее, если рассмотреть ситуацию, когда никакая настройка не применяется к элементу. Для каждого свойства браузер фактически по порядку задает два вопроса.

Во-первых: существует ли каскадируемое значение? Если какое-либо объявление этого свойства победило в каскадировании для данного элемента, используется именно это значение, и наследование не применяется.

Во-вторых, только если ответ «нет»: наследуется ли это свойство? Это полностью зависит от самого свойства, поскольку каждое из них согласно спецификации определяется как наследуемое или не наследуемое. Некоторые примеры наследуемых свойств:

color        → inherited
font-family  → inherited
font-size    → inherited

а также некоторые, которые не наследуются:

margin       → not inherited
padding      → not inherited

Такой подход сознательный. Представьте мир, в котором свойства margin наследуются: один маржинальный отступ для раздела копировался бы в каждый абзац, список и ссылку, находящиеся внутри него, что приводило бы к увеличению отступов на каждом уровне. Свойства, связанные с контейнерами, такие как margin, padding, border и width, почти всегда требуют индивидуального задания для каждого элемента, в то время как свойства, связанные с текстом, обычно должны применяться ко всей области. Если свойство не наследуется и у него нет каскадируемого значения, оно берет своё начальное значение, заданное по умолчанию для этого свойства.

Если вы не уверены относительно конкретного свойства, раздел «Наследуется» в его документации поможет прямо ответить на этот вопрос.

Дочерние элементы наследуют вычисленное значение, а не написанный вами текст

Это самая важная деталь всего процесса, лежащая в основе известного сюрприза. Предположим, родитель задает размер шрифта и процентное значение высоты строки:

.parent {
  font-size: 20px;
  line-height: 150%;
}

а дочерний элемент переопределяет только размер шрифта:

.child {
  font-size: 25px;
}

Какова же в итоге высота строки у дочернего элемента? Интуитивно кажется, что он получает сам процент:

150%

а затем применяет его к своему собственному, большему размеру шрифта:

150% of 25px

Но всё не так. Элемент наследует вычисленное значение своего родителя, а не то значение, которое было задано изначально. Для атрибута line-height процентное значение преобразуется в конкретную длину в момент вычисления значений. У родителя расчет выглядит следующим образом:

150% of 20px = 30px

следовательно, вычисленная высота строки у родителя составляет:

30px

Именно этая абсолютная длина и передаётся дальше. Потомок, несмотря на более крупный шрифт, наследует высоту строки:

30px

а не:

150% of 25px

Практическим последствием является то, что строки выглядят тесными каждый раз, когда потомок использует более крупный шрифт, чем у элемента, в котором было указано процентное значение. То же самое происходит с значениями em для line-height, поскольку они также преобразуются в длины перед наследованием. Именно поэтому рекомендуется использовать высоты строк без единиц измерения, такие как line-height: 1.5: простое число само по себе является вычисленным значением, так что каждый потомок умножает его на свой собственный размер шрифта вместо того, чтобы наследовать фиксированную длину.

Свойства, которые не наследуются, возвращаются к своему начальному значению

Теперь рассмотрим свойство из другой группы:

padding

Параметр padding не наследуется. Поэтому когда родительский элемент объявляет:

.parent {
  padding: 20px;
}

потомок не автоматически получает его значения:

padding: 20px;

Вместо этого, при отсутствии каскадирования значений и наследования, параметр padding потомка использует начальное значение свойства, которым для padding является:

0px

Это значение сохраняется, пока какое-либо другое правило явно не установит для потомка другое значение padding. Знание того, в какую категорию относится свойство, экономит время при отладке: если значение, кажется, «проникает» в потомки, это наследуемое свойство; если оно, кажется, «исчезает» у потомков, это ненаследуемое свойство. Для этого полезны инструменты разработчика браузера, поскольку во вкладке Computed можно увидеть наследуемые значения вместе с их источником — родительским элементом.

Принудительное наследование с помощью inherit

Вы можете изменить стандартное поведение любого свойства. Ключевое слово inherit указывает браузеру использовать значение родителя даже для свойства, которое обычно не наследуется:

.child {
  padding: inherit;
}

Благодаря этому правилу значение отступов дочернего элемента совпадает с вычисленными отступами родительского элемента. Это полезно, когда необходимо четко определить взаимосвязь между родителем и дочерним элементом, например, чтобы вложенный элемент использовал цвет границы или отступы своего контейнера, либо чтобы элементы формы, которые по умолчанию в стилях браузера не наследуют шрифты, использовали шрифт окружающего текста с помощью font: inherit.

Это предоставляет два различных способа получения значения родительского элемента. Автоматическое наследование применяется только к унаследованным свойствам и лишь тогда, когда отсутствует каскадирование значений. Явное наследование с использованием ключевого слова inherit само по себе является объявлением, поэтому оно имеет приоритет перед каскадированием, как и любое другое значение, и работает с любыми свойствами.

Сброс к начальному значению

Сопутствующее ключевое слово —:

initial

При применении к свойству оно сбрасывает это свойство к его начальному значению, определенному спецификацией:

.child {
  color: initial;
}

Следует помнить, что понятие «initial» означает значение по умолчанию, указанное в спецификации, а не стилишит по умолчанию браузера или значение, которое элемент мог бы иначе унаследовать. Для свойства color начальное значение — цвет текста системы (обычно чёрный), поэтому указание color: initial не восстанавливает цвет родителя элемента; оно отделяет элемент от него. Для свойства display начальное значение — inline, именно поэтому указание display: initial для элемента div превращает его в элемент типа inline, а не возвращает к формату блока.

Если рассматривать эти два ключевых слова одновременно, они выполняют противоположные функции:

inherit  → use the parent's value
initial  → reset to the property's initial value

Вместе они позволяют полностью контролировать процесс передачи значений по дереву стилей. CSS также предоставляет функции unset (которая ведёт себя как inherit для унаследованных свойств и как initial для остальных) и revert (которая возвращает элемент к стандартному шаблону стилей браузера); обе эти функции часто более полезны, чем initial, для сброса настроек компонентов.

Полная картина

Полезно рассматривать механизм наследования на фоне предшествующих ему этапов. Когда несколько правил применяются к одному и тому же элементу, браузер начинает с каскадирования, при котором объявления сравниваются в следующем порядке:

Importance
     ↓
Specificity
     ↓
Source Order

Победителем становится каскадированное значение. Затем это значение проходит через серию этапов обработки, пока не превратится в то, что может быть использовано для формирования макета и отрисовки:

Declared Value
      ↓
Cascaded Value
      ↓
Specified Value
      ↓
Computed Value
      ↓
Used Value
      ↓
Actual Value

Когда каскадируемое значение отсутствует, наследование предоставляет входные данные для этой последовательности вместо него:

No Cascaded Value
       ↓
Can Property Be Inherited?
       ↓
      Yes
       ↓
Parent's Computed Value
       ↓
Child

Строго говоря, спецификация предусматривает применение наследования в том месте, где определяется указанное значение: указанным значением является каскадируемое значение, если оно существует; в противном случае — вычисленное значение родительского элемента для наследуемых свойств; ещё в противном случае — начальное значение. После этого продолжаются обычные этапы вычисления, использования и фактического применения значения. Упрощённый общий вид этапа обработки CSS выглядит следующим образом:

CSS
                      ↓
                   Cascade
                      ↓
        ┌─────────────┴─────────────┐
        ↓                           ↓
   Importance                  Specificity
                                    ↓
                               Source Order
                                    ↓
                            Cascaded Value
                                    ↓
                            Value Processing
                                    ↓
                         Computed / Used / Actual
                                    ↓
                               Inheritance
                                    ↓
                                  CSSOM

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

Если смотреть в целом, вот полный процесс с расширенным этапом CSS:

HTML
  ↓
DOM
  ↓
CSS
  ↓
Cascade
  ↓
Specificity
  ↓
Value Processing
  ↓
Inheritance
  ↓
CSSOM
  ↓
DOM + CSSOM
  ↓
Render Tree
  ↓
Layout
  ↓
Paint
  ↓
Pixels

Учитывая эту цепочку обработки, даже небольшое правило подразумевает выполнение множества действий:

.title {
  font-size: 2rem;
  color: blue;
}

Прежде чем что-либо отрисовать, браузер должен определить:

  • какие объявления применимы к каждому элементу;
  • какое из них имеет приоритет в случае конфликта;
  • насколько специфичны конкурирующие селекторы;
  • какое значение фактически получает каждое заявление о стилях (например, сколько пикселей соответствует 2rem);
  • предоставляет ли наследование значения, которые не были явно указаны;
  • какие размеры и позиции в конечном итоге возникают на основе этих значений.
  • Только после этого страница переходит к формированию макета и отрисовке.

    CSS как система, а не список правил

    Большая часть путаницы вокруг CSS исчезает, как только вы перестаёте рассматривать её как набор изолированных правил и начинаете видеть в ней единую систему с чёткими функциями:

    • Специфичность объясняет, почему одно заявление имеет приоритет перед другим.
    • Порядок источников разрешает ситуации равенства специфичности.
    • Обработка значений преобразует написанное вами в значения, которые может использовать браузер.
  • Наследование объясняет, откуда берутся значения, если вы их не указали.
  • Вместе они превращают таблицу стилей в полный набор окончательных значений, которые используются для формирования макета и отрисовки. Даже такое краткое заявление:

    width: 66%;
    

    проходит через все эти этапы. Браузер определяет, какое правило применимо, обрабатывает процентное значение, связывает его с контейнерным блоком и определяет окончательную ширину, причем всё это происходит еще до того, как появится хотя бы один пиксель.

    После этого возникает отдельный вопрос: куда именно будет размещаться каждый элемент. Ширины, высоты, нормальный поток отрисовки, позиционирование и модель элемента относятся к визуальной модели форматирования — следующему этапу процесса, который берет эти окончательные значения и преобразует их в макет.

    Основные выводы

    • Значение свойства сначала берется из каскада стилей; наследование применяется только тогда, когда не существует соответствующего заявления.
    • Каждое свойство определяется как наследуемое (в основном связанное с текстом: color, font-*) или ненаследуемое (в основном связанное с контейнером: margin, padding).
    • Дочерние элементы наследуют вычисленное значение родителя, поэтому процентные значения или значения em для высоты строки передаются в виде фиксированных длин; предпочтительнее использовать свойство line-height без единиц измерения.
    • Свойства, которые не наследуются и для которых нет специального указания, используют своё начальное значение, например 0 для padding.
    • inherit принудительно включает наследование для любого свойства; initial возвращает значение по умолчанию, указанному в спецификации, а не значение родителя; unset и revert часто являются более подходящими инструментами для сброса настроек.