Каскад, специфичность, бокс-модель: основы глубоко
Каскад — это алгоритм, который браузер выполняет для КАЖДОГО свойства КАЖДОГО элемента на странице. «Почему этот текст серый, хотя я написал 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.
Специфичность: система 0-1-0-0
Заголовок раздела «Специфичность: система 0-1-0-0»Когда два селектора из одного источника претендуют на одно свойство, браузер считает «вес» каждого селектора. Вес — это кортеж из четырёх чисел:
специфичность = (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-1ul li {} → 0-0-0-2.nav li {} → 0-0-1-1.nav .item {} → 0-0-2-0#header .nav .item {} → 0-1-2-0style="..." → 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 — самый короткий «полный сброс» элемента, удобно для виджетов, которые должны выглядеть одинаково в любой обстановке.
Бокс-модель: content-box против border-box
Заголовок раздела «Бокс-модель: content-box против border-box»Каждый элемент — это прямоугольный бокс со слоями. Вопрос в том, что входит в заявленный размер:
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: от потока к форматированию
Заголовок раздела «display: от потока к форматированию»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.
Цвета: от hex к oklch
Заголовок раздела «Цвета: от hex к oklch».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));}calc, min, max, clamp
Заголовок раздела «calc, min, max, clamp»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’ов.
Кастомные свойства: CSS-переменные
Заголовок раздела «Кастомные свойства: CSS-переменные»: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:
// тёмная тема переключается перезаписью одного свойства, а не перегенерацией CSSdocument.documentElement.style.setProperty('--brand', 'oklch(70% 0.20 255)');Область видимости — как у любого наследуемого свойства: объявил на :root — доступно везде; объявил на .card — доступно внутри карточки и переопределяется для её потомков. Отсюда и паттерн «тема через переменные»: один и тот же компонентный CSS, разные наборы токенов на .theme-dark, .theme-light. Детали наследования, fallback’ов и JS-интеграции собраны в MDN: Использование кастомных CSS-свойств.
Типичные ошибки и грабли
Заголовок раздела «Типичные ошибки и грабли»- «Почему моё правило не применяется?» → смотри зачёркнутые объявления в DevTools. В 90% случаев победил селектор с более высокой специфичностью из сторонней библиотеки или прошлых правил. Решение — не
!important, а слабый, но побеждающий по порядку селектор. !importantкак «быстрый фикс». Один!importantрождает следующие: чужой виджет отвечает своим!important, ты — двойным. Война специфичности. Единственные легитимные случаи — utility-классы (!importantв них — осознанный контракт «утилита бьёт всё») и перекрытие inline-стилей из CMS.emдля шрифта во вложенных списках. Каждый уровень умножается на предыдущий: 1.2em × 1.2em × 1.2em. Пользуйсяremдля шрифтов,em— для локальных отступов внутри компонента.width: 100%+paddingбезborder-box. Элемент «не влезает» и создаёт горизонтальный скролл. Сброс* { box-sizing: border-box }обязателен.height: 100%«не работает». Процент высоты считается от явно заданной высоты родителя. Уbodyеё нет → 0. Решение:html, body { height: 100% }или современныйheight: 100dvh/ flex-раскладка.- Считать
ch«символом» текста.1ch— ширина цифры ноль текущего шрифта; в узких шрифтах строка из 60 букв «W» вылезет далеко за60ch. Это эвристика, а не точная мера. - Sass-переменные там, где нужны живые значения.
$gapне переопределишь в медиазапросе для конкретной карточки, а--gap— легко. Токены темы всегда на кастомных свойствах.
Вопросы на собеседовании
Заголовок раздела «Вопросы на собеседовании»- В каком порядке браузер применяет стили из разных источников?
User-agent → user → author (по специфичности, затем по порядку в коде) → author
!important→ inline → inline!important. Ключевое:!importantпереворачивает порядок источников для этой декларации. - Сколько классов нужно, чтобы победить один
#id? Необходимое число не существует: специфичность сравнивается лексикографически, и0-1-0-0бьёт любое0-0-N-0. Поэтому айди в CSS — антипаттерн. - Чем
inheritотличается отunsetиrevert?inherit— взять вычисленное значение родителя.unset— наследуемые свойства какinherit, остальные какinitial.revert— откатиться к стилям предыдущего источника каскада (обычно user-agent’а). - Что меняет
box-sizing: border-boxи почему его все ставят? Вwidthначинают входить padding и border вместо того, чтобы добавляться к нему. Задуманные 200px остаются 200px на экране. Без него все «50% + padding» считаются арифметикой с переполнением. - Когда
em, а когдаrem?rem— для размеров шрифта и всего, что должно масштабироваться от настроек пользователя предсказуемо.em— для локальных пропорций внутри компонента (иконка рядом с текстом, отступы внутри кнопки), где она должна реагировать на размер текста самого элемента. - Чем кастомные свойства отличаются от переменных Sass?
Sass заменяется при сборке и заморожен, кастомные свойства живут в браузере: наследуются каскадом, доступны из JS, работают с медиазапросами и псевдоклассами, имеют область видимости по DOM и fallback через второй аргумент
var(). - Какое свойство наследуется, а какое нет — и почему? Наследуется то, что имеет смысл «протекать» внутрь текста (font, color, line-height, visibility, custom properties). Не наследуется геометрия и оформление боксов (width, margin, border, background) — иначе каждый вложенный элемент дублировал бы рамки и фон родителя.
- Зачем нужен
oklch, если естьhsl? В hsl яркость (L) не равномерна между оттенками: жёлтый L=60% субъективно светлее синего L=60%. oklch построен на восприятии, поэтому палитры токенов в нём получаются ровными по контрасту — критично для дизайн-систем и a11y.
Практика
Заголовок раздела «Практика»- Отладка каскада. Сверстай
.cardс текстом и ссылкой внутри. Внешний файл задаётa { color: blue }, а тебе нужны ссылки в цвет текста. Реши черезinheritи через увеличение специфичности; объясни разницу в подходах. Критерий: оба варианта работают, ты можешь показать в DevTools, почему победил каждый. - Специфичность. Напиши четыре селектора с возрастающей специфичностью, примени к одному элементу четыре разных цвета. Проверь в DevTools, что реально применилось. Затем переведи побеждающий селектор на
:where()и убедись, что он перестал побеждать. Критерий: предсказываешь победителя до перезагрузки страницы. - Бокс-модель. Сверстай сетку из двух колонок по
50%сpadding: 20pxиborder: 5px. Покажи горизонтальный скролл безborder-boxи его исчезновение со сбросом. Критерий: объясни на пальцах, откуда взялись лишние 50px. - Единицы. Сверстай вложенный список трёх уровней, задав
li { font-size: 1.3em }, и тот же список с1.3rem. Замерь результаты в DevTools → Computed. Критерий: разница в поведении описана своими словами. - Тема на кастомных свойствах. Сделай карточку, кнопку и ссылку, цвета которых берутся из
--brand-*на:root. Добавь класс.theme-dark, переопределяющий токены, и переключатель темы на JS черезsetProperty. Критерий: тема меняется без перезагрузки и без дублирования CSS-компонентов.
Что почитать
Заголовок раздела «Что почитать»- MDN: Cascade, specificity and inheritance — авторитетный разбор всех уровней каскада с примерами.
- MDN: Специфичность — формальные правила подсчёта и псевдоклассы с аномалиями.
- Every Layout — Box-sizing — ментальная модель бокса без магии.
- CSS Tricks: A Complete Guide to Custom Properties — области видимости, fallback’и, JS-интеграция.
- OKLCH color picker — интерактивная демонстрация равномерности oklch против hsl.
- web.dev: Modern CSS colors — color spaces, гамма и почему hsl — прошлый век для дизайн-токенов.