Суд: почему твой CSS «не работает»
Ты написал правило, браузер его проигнорировал, ты дописал !important и пошёл дальше. Его не проигнорировали — оно проиграло дело по процедуре, которую можно выучить за вечер.
Ставишь элементу color: red. Ничего не происходит. Добавляешь ещё один класс. Опять ничего. Пишешь !important, текст краснеет, и ты идёшь дальше с лёгким ощущением, что CSS произволен.
Он не произволен. За каждое свойство каждого элемента боролись несколько правил, и браузер разрешил конфликт по жёсткой, опубликованной процедуре. Твоё правило не сломалось — оно проиграло, а кому именно, ты так и не выяснил.
Это крупнейший источник потерянных часов у тех, кто учит фронтенд, и вызван он одним заблуждением: будто CSS — это список команд. Он не список команд. CSS — это набор притязаний, а каскад — суд, который их разрешает.
Порядок рассмотрения
Для каждого свойства каждого элемента браузер сортирует все притязания по этой процедуре и берёт победителя:
- Источник и важность. Твои стили бьют умолчания браузера.
!importantне добавляет силы внутри уровня, а переворачивает весь уровень целиком — поэтому!importantв стилях библиотеки можно побить только другим!important, и поэтому эскалация никогда не заканчивается. - Специфичность. Считаемый счёт, о нём ниже.
- Порядок в исходнике. При прочих равных выигрывает то, что ниже. Отсюда вся важность порядка подключения файлов и то, почему твоё переопределение начинает работать, стоит его переставить под переопределяемое.
Стоит заметить две вещи, которых в списке нет, потому что новички уверены в обеих: сколько селекторов ты написал — не ступень, и как недавно ты сохранил файл — тоже.
Специфичность — это тройка, а не число
Специфичность считается тремя числами, которые сравнивают слева направо, как номер версии:
- Идентификаторы —
#header - Классы, атрибуты, псевдоклассы —
.card,[disabled],:hover - Элементы и псевдоэлементы —
div,a,::before
То есть .card .title даёт 0-2-0, а #main p — 1-0-1. Выигрывает второй, хотя классов у него меньше: левый разряд решает первым, и никакое количество классов в него не перелезает. Именно это интуиция угадывает неверно: сто классов всё равно проигрывают одному идентификатору.
Два следствия, с которыми жить ежедневно. Идентификатор в селекторе — обещание, что перебить его вежливо ты уже не сможешь; поэтому большинство кодовых баз оформляет только классами. А !important стоит над всей шкалой — это не сильный довод, это выход из зала суда и принятие закона. Иногда оправдано; почти всегда — долг, который кто-то отдаст позже, обычно вторым !important.
Если встретишь в современном проекте @layer — это взрослый ответ на всё вышеописанное: явные именованные слои в заданном тобой порядке, где притязания целого слоя взвешиваются раньше, чем вообще заходит речь о специфичности. Он появился ровно потому, что войны специфичности были нормой.
Наследование: что проваливается вниз, а что нет
Задай color элементу body — покраснеет вся страница. Задай body рамку — ни у чего внутри рамки не появится.
Это не непоследовательность. Свойства про текст — цвет, шрифт, размер, межстрочный интервал, выравнивание — наследуются вниз по дереву, потому что этого почти всегда и хотят. Свойства про коробку — рамка, внутренние и внешние отступы, фон, ширина — не наследуются, потому что наследуемая рамка была бы абсурдом.
Управлять этим явно позволяют два ключевых слова: inherit намеренно берёт значение родителя, initial сбрасывает к встроенному умолчанию свойства. А ещё наследование — причина того, что элементы форм выглядят чужеродно, пока им не скажешь обратного: input, select и button по умолчанию не наследуют шрифт страницы, и поэтому почти в каждом наборе стилей на земле есть строка, которая их к этому принуждает.
Блочная модель и строка, которую пишут все
Каждый элемент — прямоугольник из четырёх слоёв: содержимое в середине, внутренний отступ вокруг него внутри рамки, рамка и внешний отступ — пустое место снаружи.
Историческая ловушка: по умолчанию width задаёт ширину только содержимого. Дай коробке 300 пикселей ширины, 20 внутреннего отступа и 1 рамки — она займёт 342. Поэтому почти каждый проект начинается с box-sizing: border-box для всего: после этого width означает ширину всей видимой коробки, ровно как ты и предполагал.
Внешние отступы приносят собственную странность: вертикальные отступы соседних блоков схлопываются в один, берётся больший, а не сумма. 20 снизу у одного абзаца и 30 сверху у следующего дают 30, а не 50. Это удивляет каждого ровно один раз и полностью исчезает во флекс- и грид-контейнерах, где отступы не схлопываются.
Пять причин, по которым правило «не применяется»
Когда стиль упрямо не срабатывает, это почти всегда одно из пяти, и пройтись по списку по порядку быстрее любых попыток наугад:
- Победило что-то более специфичное. Инструменты разработчика показывают проигравшее правило перечёркнутым — это весь ответ, видимый за две секунды.
- Порядок в исходнике. Та же специфичность, а ниже сказано иначе.
- Селектор попадает не туда, куда ты думаешь. Опечатка, не та вложенность, элемент, который рисует JavaScript и которого ещё нет.
- Свойство неприменимо к этому элементу.
widthу строчного элемента не делает ничего;justify-contentне делает ничего вне флекс- или грид-контейнера;z-indexне делает ничего при статическом позиционировании. Половина «CSS сломан» — это свойство, применённое к контексту, который его игнорирует. - Значение недопустимо. Одна опечатка в значении — и браузер молча выбрасывает одну эту декларацию. Ни ошибки, ни предупреждения: её пометят инструменты разработчика, и больше ничто.
Обрати внимание: ни одна из пяти причин не лечится добавлением !important. Его добавляют тогда, когда решили не выяснять, какая из пяти это была.
CSS никогда «не работает» неправильно. Он делает ровно то, что сказано правилами. Просто тебя не было в зале, когда дело рассматривали.
Учи четыре механизма, а не четыреста рецептов
У CSS репутация языка, который учится только накоплением приёмов, — и репутацию эту делает способ, которым его обычно учат: загуглил проблему, скопировал кусок, пошёл дальше. Сегодня работает, а учит нулю: скопированный кусок — заученный снимок с одного ракурса, а задача следующей недели встанет под другим углом.
Ответ сферы «Как учиться» ложится сюда почти неприлично точно. Понимать — значит уметь менять масштаб на одном и том же объекте: отъехать к вопросу что вообще делает CSS (присваивает по одному значению каждому свойству каждого элемента) и въехать в вопрос как этот элемент получил это значение (четыре механизма: каскад, специфичность, наследование, блочная модель). Любое свойство, которое ты когда-либо встретишь, вставляется в эту рамку. Выучишь рамку — новое свойство стоит минут; пропустишь — каждое навсегда останется отдельным фактом для зубрёжки.
Здесь же есть редкая для обучения вещь: мгновенный и честный внешний судья. Ты выдвигаешь гипотезу, почему элемент выглядит так, проверяешь её в инспекторе и через пять секунд оказываешься прав или неправ. Это осознанная практика с обратной связью более плотной, чем почти в любом другом предмете, — но только если предсказание сделано заранее. Менять значения наугад, пока не станет похоже, даёт тот же результат и нулевое обучение.
На практике
Сначала инспектируй, потом правь. Когда что-то не так — открой инструменты разработчика, выбери элемент и прочитай панель стилей сверху вниз: что применилось, что перечёркнуто, каким получилось вычисленное значение. Неделя такой привычки перестраивает мышление о CSS сильнее любого учебника.
Предсказывай, потом проверяй. До открытия инспектора проговори, почему, по-твоему, так происходит. Ошибиться с заявленным предсказанием стоит в разы больше, чем получить готовый ответ.
Запрети себе !important на месяц. Не навсегда — на месяц, пока учишься. Каждый раз, когда его хочется, выясняй, какая из пяти причин это была на самом деле. Одно это ограничение учит специфичности быстрее, чем чтение про специфичность.
Оформляй только классами. Никаких идентификаторов в селекторах, никаких элементных селекторов в глубине компонентов. Держи специфичность плоской — и большинство каскадных драк просто не случится.
Собери небольшой компонент по скриншоту, потом открой оригинал и сравни свойство за свойством. Скопировать, а потом восстановить по памяти — техника из сферы «Как учиться» в самой дешёвой форме, и CSS подходит для неё необычайно хорошо.
Проговори блочную модель вслух, глядя на схему коробки в инструментах разработчика: содержимое, внутренний отступ, рамка, внешний отступ. Через три дня проговори снова, не глядя. Это четыре слова, и пользоваться ими ты будешь ежечасно.
Проверь себя
Закрой статью и ответь своими словами:
- Каковы три ступени каскада по порядку?
- Почему
#main pпобеждает.card .title .text? - Что
!importantна самом деле делает с процедурой и почему это долг? - Какие свойства наследуются, а какие нет — и какая логика за этим делением?
- Что меняет
box-sizing: border-boxи почему это ставят почти во всех проектах? - Перечисли пять причин, по которым правило может не примениться.
- Почему «добавить
!important» — ответ того, кто решил не ставить диагноз?
Коротко
- CSS — не список команд, а набор притязаний; каскад — процедура, решающая, чьё притязание выиграет для каждого свойства каждого элемента.
- Порядок: источник и важность, затем специфичность, затем порядок в исходнике. Количество селекторов и свежесть файла ступенями не являются.
- Специфичность — тройка: идентификаторы, потом классы/атрибуты/псевдоклассы, потом элементы, — сравнивается слева направо, поэтому сто классов проигрывают одному идентификатору.
- Свойства про текст наследуются, про коробку — нет;
inheritиinitialуправляют этим явно. - Блочная модель: содержимое, внутренний отступ, рамка, внешний отступ;
border-boxвозвращаетwidthтот смысл, который ты и предполагал, а вертикальные отступы между блоками схлопываются к большему. - «Правило не работает» — это одно из пяти: специфичность, порядок, непопадающий селектор, свойство, неприменимое к контексту, или недопустимое значение.
!important— не починка, а решение не ставить диагноз, и он эскалирует.- Учи четыре механизма вместо четырёхсот рецептов и пользуйся инспектором как пятисекундной обратной связью — после высказанного предсказания, а не вместо него.