Перейти к содержимому

Каскад, специфичность, бокс-модель: основы глубоко

Каскад — это алгоритм, который браузер выполняет для КАЖДОГО свойства КАЖДОГО элемента на странице. «Почему этот текст серый, хотя я написал color: red?» — вопрос не про твой код, а про каскад: где-то селектор с более высоким приоритетом победил, и браузер честно применил его. В краткой версии учебника ты видел CSS как набор правил «селектор → свойства». Теперь смотрим под капот: откуда вообще берутся стили, в каком порядке они соревнуются, и почему «высокая специфичность» — это не комплимент.

В продакшене каскад — источник половины «невозможных» багов. Сторонний виджет переопределяет твою типографику, !important из inline-стиля из CMS ломает твою тему, а em во вложенном списке раздувает шрифт до 40px. Разработчик, понимающий каскад, читает такие баги как открытую книгу; не понимающий — добавляет ещё один !important и усугубляет войну.

Идём от общего к частному: источники стилей → специфичность → наследование → бокс-модель → display → единицы → цвета → вычисления → кастомные свойства.

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

# Источник Откуда берётся Пример
1 User-agent Встроенные стили браузера У <h1> есть дефолтный font-size: 2em, у <ul>padding-left: 40px
2 User Настройки пользователя Кастомный шрифт, выбранный в браузере; prefers-reduced-motion
3 Author (normal) Твой CSS-файл .card { color: #222 }
4 Author !important Твой CSS с флагом .card { color: #222 !important }
5 Inline + inline !important Атрибут style="" <div style="color: red">

Внутри ОДНОГО источника решают три критерия, в порядке: специфичность → порядок в коде (позже написанное побеждает). !important из inline-стиля переопределить невозможно — разве что тем же !important в более позднем правиле с той же специфичностью. Первоисточник по всем правилам этого раздела — спецификация CSS Cascading and Inheritance Level 5; дружелюбная версия тех же правил — MDN: Каскад, специфичность и наследование.

Есть ещё уровни @layer (Cascade Layers), которые вставляются между user и author-источниками: незалэеренный author-CSS бьёт любой layer. Это современный способ «спустить» стили библиотеки ниже твоих — но про это подробно в разделе про современный CSS.

Когда два селектора из одного источника претендуют на одно свойство, браузер считает «вес» каждого селектора. Вес — это кортеж из четырёх чисел:

специфичность = (inline, id, class/attr/pseudo-class, element/pseudo-element)
( a , b , c , d )

Сравнение — лексикографическое, как версии пакетов: сначала сравниваем a, при равенстве — b, и так далее. 0-1-0-0 (один #id) бьёт 0-0-10-0 (десять классов) — классы не «складываются» против айди.

p { color: black } /* 0-0-0-1 */
.article p { color: gray } /* 0-0-1-1 */
#content p { color: blue } /* 0-1-0-1 ← победит */
<div style="color:red"> /* 1-0-0-0 ← победит над всеми */

Растущая лестница на одном элементе:

* {} → 0-0-0-0 универсальный, почти не участвует
li {} → 0-0-0-1
ul li {} → 0-0-0-2
.nav li {} → 0-0-1-1
.nav .item {} → 0-0-2-0
#header .nav .item {} → 0-1-2-0
style="..." → 1-0-0-0

Практическое правило индустрии: не используй #id в CSS вообще. Айди появляется в разметке один раз и предназначен для якорей и JS-хуков. В вёрстке он даёт неподъёмный вес 0-1-0-0, который потом невозможно переопределить без второго айди или !important.

Ряд свойств передаётся от родителя к потомкам автоматически: типографика (font-*, line-height, color, text-align), visibility, cursor, кастомные свойства (--*). Всё, что рисует рамки и фон (border, background, width, margin, padding), НЕ наследуется — иначе каждый <div> внутри карточки таскал бы её серый фон.

Управлять наследованием явно можно четырьмя ключевыми словами:

Ключевое слово Что делает Типичный пример
inherit Возьми вычисленное значение родителя a { color: inherit } — ссылки в цвет текста вокруг
initial Значение по умолчанию спецификации для свойства all: initial — сбросить всё
unset Для наследуемых — как inherit, для ненаследуемых — как initial универсальный «сброс к естественному»
revert Откат к предыдущему уровню каскада (user-agent или user-стилям) «Верни браузерный дефолт»

Свойство-помощник all применяет ключевое слово сразу ко всем: all: unset — самый короткий «полный сброс» элемента, удобно для виджетов, которые должны выглядеть одинаково в любой обстановке.

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

box-sizing: content-box (дефолт браузера) box-sizing: border-box (стандарт индустрии)
┌─────────────── width = 200 ────────────────┐ ┌─────────────── width = 200 ────────────────┐
│ ┌────────────────────────────────────────┐ │ │ ┌─ padding 20 ─────────────────────────┐ │
│ │ content │ │ │ │ ┌──── content (140) ───────────────┐ │ │
│ │ 200 × 200 │ │ │ │ │ │ │ │
│ └────────────────────────────────────────┘ │ │ │ │ │ │ │
└────────────────────────────────────────────┘ │ │ └──────────────────────────────────┘ │ │
▲ padding 20 снаружи width └──────────────────────────────────────┘ │
▲ border 10 снаружи ▲ border 10 ВНУТРИ width │
▲ margin 15 снаружи всего ▲ margin 15 снаружи └─
реальная ширина на экране: 200+40+20 = 260 реальная ширина на экране: ровно 200

При content-box width описывает только контент — отсюда бесконечные «я задал 50%, а оно не влезло» из-за padding и border. При border-box width — это вся видимая ширина бокса, padding и border «съедают» контент внутрь. Поведение каждого режима наглядно показано на странице MDN: box-sizing.

/* первые две строки практически любого reset.css: */
*, *::before, *::after {
box-sizing: border-box;
}

margin при этом всегда снаружи и на ширину не влияет — он создаёт «воздух» между боксами. Вертикальные margin двух соседних блоков схлопываются: не 20+30=50, а max(20,30)=30.

display определяет, как элемент участвует в раскладке. Базовые значения:

Значение Поведение Ширина Новая строка
block Занимает всю строку 100% родителя да (сам создаёт)
inline Течёт в тексте по контенту нет
inline-block Гибрид: в строке, но как бокс по контенту, настраивается нет
none Убран из дерева и раскладки
flow-root Блок, создающий новый BFC 100% да

Современные значения — flex и grid (отдельные главы). Важно понимать связку: display: inline-block наследует главный глюк инлайнов — пробелы в разметке между такими элементами становятся «буквой» шириной в 4px, поэтому ряды кнопок/карточек на inline-block «не сходятся по пикселю». Полный список внешних и внутренних значений display — на странице MDN: display.

flow-root — современная замена хака overflow: hidden для «схлопывания» флоатов и margin’ов: создаёт новый Block Formatting Context без побочных эффектов обрезки.

.card {
display: flow-root; /* дочерние float'ы не выпрыгнут, margin'ы не схлопнутся */
}
Единица От чего считается Когда использовать
px физический пиксель (через devicePixelRatio) границы, мелкая графика, breakpoint’ы медиазапросов
rem от font-size корня <html> (обычно 16px) типографика, отступы, всё что должно масштабироваться от настроек пользователя
em от font-size ТЕКУЩЕГО элемента (в font-size — родителя) локальные отступы иконок/бейджей внутри компонента
% от размера родителя (для padding — от ширины!) ширины колонок, высоты внутри flex/grid
vw/vh 1% ширины/высоты вьюпорта полноэкранные секции, fluid-типографика
ch ширина символа «0» текущего шрифта оптимальная длина строки текста: max-width: 60ch

Грабля вложенного em — она копится:

li { font-size: 1.2em } /* от родителя: 16 → 19.2 */
li li { font-size: 1.2em } /* уже от 19.2 → 23.04 */
li li li { font-size: 1.2em } /* 27.6… */

Для типографики от em отказались почти везде: используй rem для размеров шрифта и em — только для локальных пропорций внутри одного компонента. И не забудь: у многих пользователей в браузере стоит дефолтный шрифт не 16px — поэтому font-size: 14px на <html> — это хамство, вместо этого 62.5%-трюк или просто живи с rem.

.card {
color: #1a202c; /* hex — по-прежнему ок для констант */
color: rgb(26 32 44 / 0.9); /* modern: пробелы + альфа через слэш */
color: hsl(220 20% 14%); /* тон/насыщенность/светлота — удобно варьировать */
color: lab(14% -2 -10); /* perceptually uniform */
color: oklch(22% 0.03 255); /* самая равномерная модель, стандарт для токенов */
}

Почему современные модели (oklch, oklab, lab) важны: в hsl светлый зелёный субъективно ярче светлого синего при одинаковой L. В oklch L=60% выглядит одинаково светлым для любого оттенка — поэтому дизайн-системы 2024+ строят палитры именно в oklch: меняешь hue — получаешь ровную шкалу.

:root {
--hue: 255;
--brand-100: oklch(95% 0.03 var(--hue));
--brand-500: oklch(60% 0.20 var(--hue));
--brand-900: oklch(25% 0.10 var(--hue));
}

CSS умеет считать в рантайме браузера:

.sidebar {
width: clamp(200px, 25vw, 320px); /* не уже 200, не шире 320, между — 25vw */
}
.article {
font-size: clamp(1rem, 0.6rem + 0.8vw, 1.25rem); /* fluid-типографика, подробности — в главе про адаптив */
}
.hero {
min-height: min(90vh, 700px); /* хотим высокий, но не выше 700 */
padding: calc(1rem + 2%); /* смешиваем единицы свободно */
}

clamp(min, preferred, max) — главный инструмент «безмедиазапросной» адаптивности: одна строка вместо трёх breakpoint’ов.

:root {
--brand: oklch(60% 0.20 255);
--radius: 8px;
--space: 1rem;
}
.btn {
background: var(--brand);
border-radius: var(--radius, 4px); /* fallback: 4px, если --brand не определён */
padding: calc(var(--space) / 2) var(--space);
}
.card {
--brand: oklch(60% 0.20 160); /* переопределение в области видимости */
}
.card .btn { background: var(--brand); } /* теперь зелёная, а не синяя */

В отличие от Sass-переменных ($brand: #00f — заменяется на этапе сборки и замораживается), кастомные свойства — живые: они наследуются каскадом, их можно менять в медиазапросах, в состояниях :hover и — главное — из JavaScript:

// тёмная тема переключается перезаписью одного свойства, а не перегенерацией CSS
document.documentElement.style.setProperty('--brand', 'oklch(70% 0.20 255)');

Область видимости — как у любого наследуемого свойства: объявил на :root — доступно везде; объявил на .card — доступно внутри карточки и переопределяется для её потомков. Отсюда и паттерн «тема через переменные»: один и тот же компонентный CSS, разные наборы токенов на .theme-dark, .theme-light. Детали наследования, fallback’ов и JS-интеграции собраны в MDN: Использование кастомных CSS-свойств.

  1. «Почему моё правило не применяется?» → смотри зачёркнутые объявления в DevTools. В 90% случаев победил селектор с более высокой специфичностью из сторонней библиотеки или прошлых правил. Решение — не !important, а слабый, но побеждающий по порядку селектор.
  2. !important как «быстрый фикс». Один !important рождает следующие: чужой виджет отвечает своим !important, ты — двойным. Война специфичности. Единственные легитимные случаи — utility-классы (!important в них — осознанный контракт «утилита бьёт всё») и перекрытие inline-стилей из CMS.
  3. em для шрифта во вложенных списках. Каждый уровень умножается на предыдущий: 1.2em × 1.2em × 1.2em. Пользуйся rem для шрифтов, em — для локальных отступов внутри компонента.
  4. width: 100% + padding без border-box. Элемент «не влезает» и создаёт горизонтальный скролл. Сброс * { box-sizing: border-box } обязателен.
  5. height: 100% «не работает». Процент высоты считается от явно заданной высоты родителя. У body её нет → 0. Решение: html, body { height: 100% } или современный height: 100dvh / flex-раскладка.
  6. Считать ch «символом» текста. 1ch — ширина цифры ноль текущего шрифта; в узких шрифтах строка из 60 букв «W» вылезет далеко за 60ch. Это эвристика, а не точная мера.
  7. Sass-переменные там, где нужны живые значения. $gap не переопределишь в медиазапросе для конкретной карточки, а --gap — легко. Токены темы всегда на кастомных свойствах.
  1. В каком порядке браузер применяет стили из разных источников? User-agent → user → author (по специфичности, затем по порядку в коде) → author !important → inline → inline !important. Ключевое: !important переворачивает порядок источников для этой декларации.
  2. Сколько классов нужно, чтобы победить один #id? Необходимое число не существует: специфичность сравнивается лексикографически, и 0-1-0-0 бьёт любое 0-0-N-0. Поэтому айди в CSS — антипаттерн.
  3. Чем inherit отличается от unset и revert? inherit — взять вычисленное значение родителя. unset — наследуемые свойства как inherit, остальные как initial. revert — откатиться к стилям предыдущего источника каскада (обычно user-agent’а).
  4. Что меняет box-sizing: border-box и почему его все ставят? В width начинают входить padding и border вместо того, чтобы добавляться к нему. Задуманные 200px остаются 200px на экране. Без него все «50% + padding» считаются арифметикой с переполнением.
  5. Когда em, а когда rem? rem — для размеров шрифта и всего, что должно масштабироваться от настроек пользователя предсказуемо. em — для локальных пропорций внутри компонента (иконка рядом с текстом, отступы внутри кнопки), где она должна реагировать на размер текста самого элемента.
  6. Чем кастомные свойства отличаются от переменных Sass? Sass заменяется при сборке и заморожен, кастомные свойства живут в браузере: наследуются каскадом, доступны из JS, работают с медиазапросами и псевдоклассами, имеют область видимости по DOM и fallback через второй аргумент var().
  7. Какое свойство наследуется, а какое нет — и почему? Наследуется то, что имеет смысл «протекать» внутрь текста (font, color, line-height, visibility, custom properties). Не наследуется геометрия и оформление боксов (width, margin, border, background) — иначе каждый вложенный элемент дублировал бы рамки и фон родителя.
  8. Зачем нужен oklch, если есть hsl? В hsl яркость (L) не равномерна между оттенками: жёлтый L=60% субъективно светлее синего L=60%. oklch построен на восприятии, поэтому палитры токенов в нём получаются ровными по контрасту — критично для дизайн-систем и a11y.
  1. Отладка каскада. Сверстай .card с текстом и ссылкой внутри. Внешний файл задаёт a { color: blue }, а тебе нужны ссылки в цвет текста. Реши через inherit и через увеличение специфичности; объясни разницу в подходах. Критерий: оба варианта работают, ты можешь показать в DevTools, почему победил каждый.
  2. Специфичность. Напиши четыре селектора с возрастающей специфичностью, примени к одному элементу четыре разных цвета. Проверь в DevTools, что реально применилось. Затем переведи побеждающий селектор на :where() и убедись, что он перестал побеждать. Критерий: предсказываешь победителя до перезагрузки страницы.
  3. Бокс-модель. Сверстай сетку из двух колонок по 50% с padding: 20px и border: 5px. Покажи горизонтальный скролл без border-box и его исчезновение со сбросом. Критерий: объясни на пальцах, откуда взялись лишние 50px.
  4. Единицы. Сверстай вложенный список трёх уровней, задав li { font-size: 1.3em }, и тот же список с 1.3rem. Замерь результаты в DevTools → Computed. Критерий: разница в поведении описана своими словами.
  5. Тема на кастомных свойствах. Сделай карточку, кнопку и ссылку, цвета которых берутся из --brand-* на :root. Добавь класс .theme-dark, переопределяющий токены, и переключатель темы на JS через setProperty. Критерий: тема меняется без перезагрузки и без дублирования CSS-компонентов.