Селекторы и каскад: соберите судью, который решает, чей цвет
Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.
Цвет одного абзаца могут задавать сразу несколько источников: правило по тегу, правило по классу, правило по id, атрибут style и стили самого браузера. На экране побеждает один, и выбирает его не «кто конкретнее на глаз», а короткий список вопросов, которые браузер задаёт всегда в одном порядке. Этот список и есть каскад — и, зная его, вы перестаёте подбирать селекторы перебором. Сначала выучим язык, которым правила называют элементы, потом соберём судью по пунктам и каждый пункт проверим на примере.
Правило CSS состоит из двух половин. Слева селектор — кого оформить, справа, в фигурных скобках, объявления — как: p { color: rgb(0, 0, 255); } красит синим все абзацы. Правила собирают в элементе <style> — в примерах он стоит в начале разметки. Цвет записан тремя числами от 0 до 255, долями красного, зелёного и синего: rgb(255, 0, 0) — красный, rgb(0, 128, 0) — зелёный. В таком виде браузер отдаёт цвет и наружу, даже если в правиле было написано слово red, — поэтому в ответах ниже всюду тройки чисел.
Строка в кавычках, которую вы передаёте замеру, — это тоже селектор: прибор находит на странице ровно те элементы, которые оформило бы правило с таким селектором. Один язык на стили и на проверку, и это удобно: написав селектор для правила, вы уже знаете, как спросить о нём.
| Селектор | Кого выбирает |
|---|---|
li | все элементы <li> — селектор по тегу |
.done | элементы, у которых в атрибуте class есть слово done; классов у элемента может быть несколько, через пробел |
#menu | элемент с id="menu"; такой id на странице должен быть один |
[data-id] | элементы, у которых есть атрибут data-id, с любым значением |
[type="checkbox"] | элементы, у которых атрибут type равен ровно checkbox |
li.done | элемент, у которого сходится всё сразу: и тег li, и класс done |
ul li | li где угодно внутри ul, на любой глубине, — потомок |
ul > li | только прямые дети ul — ребёнок |
li:first-child | li, который стоит первым среди детей своего родителя |
* | любой элемент |
a, span | и ссылки, и span: запятая собирает селекторы в список |
Слово после двоеточия — псевдокласс. Он выбирает не по тому, что написано в разметке, а по положению или состоянию элемента: :first-child — первый ребёнок, :disabled — выключенное поле. Псевдоклассов десятки, остальные появятся там, где понадобятся.
Проверим словарь на списке покупок. Предскажите пять чисел — и не спешите: подвох здесь в двух местах. Один из элементов с классом done вовсе не li, а второй лежит на уровень глубже, чем кажется.
done есть у двоих: у пункта «Хлеб» и у span внутри «Молока». Селектор li.done без пробела требует тег и класс от одного элемента — остаётся «Хлеб». #menu > .done тоже находит одного: span приходится списку внуком, а не ребёнком. li:first-child читается как «li, который сам первый ребёнок», а не «первый ребёнок внутри li», поэтому это «Хлеб», а не «и сыр».Пробел в селекторе — не форматирование. li.done — один элемент с тегом и классом, li .done — элемент с классом где-то внутри li. Разница в один символ, а находят они разное.
Кавычки у значения атрибута можно опустить, только если значение — слово: [type=checkbox] сработает, а [data-id=3] браузер отвергнет как недопустимый селектор, и вызов закончится ошибкой. Надёжнее ставить кавычки всегда. Внутри строки в двойных кавычках удобно брать одинарные: count("[type='checkbox']").
Этот язык пригодится не только в стилях: в части II JavaScript ищет элементы на странице теми же селекторами.
Селекторы разных правил часто задевают один и тот же элемент, и тогда правила спорят за одно свойство. Самый простой случай — два одинаковых селектора: побеждает тот, что ниже в коде. Но это последний довод судьи, а не первый, и опираться на порядок строк — значит признать поражение заранее. Предскажите цвета обоих абзацев: у второго есть класс, а правило для класса стоит выше обоих правил для тега.
p равны, и выигрывает нижнее. Второй зелёный, хотя правило .note стоит в самом верху: класс весит больше тега, и до порядка в коде дело не дошло. Этот вес называется специфичностью.Специфичность — три отдельных счётчика, их записывают тройкой (a, b, c):
| Счётчик | Что считается | Пример |
|---|---|---|
| a | id | #menu a → (1, 0, 1) |
| b | классы, атрибуты, псевдоклассы | .nav .link.active → (0, 3, 0) |
| c | теги | ul li → (0, 0, 2) |
| — | *, пробел и > не весят ничего | #menu > li:first-child → (1, 1, 1) |
Сравнивают счётчики слева направо. У кого больше id, тот и сильнее, сколько бы классов ни набрал соперник; второй счётчик смотрят, только если равны первые, третий — если равны и вторые. Отсюда и главная практическая привычка: не спорить с чужим id своим классом, а писать селекторы так, чтобы id в них не появлялся вовсе. В меню ниже три правила для ссылок: первая ссылка лежит в #menu, вторая — в таком же списке, но без id. Посчитайте специфичность каждого правила и предскажите оба цвета.
#menu a (1, 0, 1) бьёт .nav .link.active (0, 3, 0): три класса против одного id проигрывают на первом же счётчике. У второй ссылки правило с id не срабатывает, и спорят (0, 3, 0) и .nav a (0, 1, 1). Нижнее правило проигрывает: порядок в коде решает только при полном равенстве.Бывает нужен селектор, который не весит ничего. Это :where(…): он выбирает те же элементы, что список в скобках, но его специфичность — (0, 0, 0), даже если внутри id. Так пишут стили, которые должны уступать любому вашему правилу, — сбросы и библиотеки. Встретив :where() в чужом коде, не удивляйтесь: автор заранее позаботился, чтобы его правило можно было перебить одним классом.
Можно ли так писать, решает Baseline — так же, как в HTML. Номера версии нет и у CSS: «CSS3» — ярлык 2010-х, а W3C выпускает снимки того, что сейчас входит в язык, последний из них — CSS Snapshot 2026. Поэтому о каждой возможности спрашивают отдельно: widely — пишите, newly — знайте, limited — не рассчитывайте. :where() — Baseline widely с 21.07.2023, пишите смело.
Если встретите в чужом CSS правило, вложенное в другое, помните две ловушки. Знак & там означает селектор внешнего правила. В Sass запись &__title внутри .card склеивается в имя класса card__title, а браузер имена из кусков не собирает, и такое правило ничего не покрасит. И & весит как самый сильный селектор из списка внешнего правила: внутри #box, .card он тянет вес id, даже если элемент нашёлся по классу.
Как устроена сама вложенность и чем она отличается от Sass, глава разберёт ближе к концу — вложенность правил в CSS.
Атрибут style в разметке — его ещё называют инлайн-стилем — сильнее любого селектора: он принадлежит самому элементу, и судья смотрит на него раньше, чем начинает считать id и классы. А слово !important после значения поднимает объявление ещё выше: важное объявление бьёт все обычные — и правила с id, и атрибут style. Оба способа работают, и оба делают стиль нечитаемым: следующий человек не поймёт, почему его правило не применилось.
У обоих абзацев ниже есть id и атрибут style, а у второго ещё и класс, для которого цвет объявлен важным. Какого цвета каждый?
#first проиграло атрибуту, хотя в нём id. Второй зелёный: у .loud всего один класс, но с !important, и против него атрибут style бессилен.Каждое !important в проекте рождает следующее: перебить важное объявление можно только другим важным. Если правило не побеждает, сначала посчитайте специфичность — чаще всего хватает одного класса.
Теперь всё вместе. Пять правил, три элемента: два абзаца и span. Измеряем детей карточки — .card > *, то есть только прямых детей. Предскажите три цвета по порядку и не пропустите, что спрятано внутри :where().
p { … !important } перебил и #lead, и атрибут style второго абзаца. span под правило для p не попадает, и за него спорят .note и :where(#box) .note. Id внутри :where() не весит ничего, у обоих правил (0, 1, 0), и побеждает нижнее — оранжевое. А .card p проиграло везде, где участвовало.В большом проекте стили приходят из разных мест: сброс, библиотека компонентов, стили страницы. Воевать с чужими селекторами специфичностью неудобно — побеждает тот, кто придумал селектор длиннее, и конца этой гонке нет. Поэтому в CSS есть слои. Строка @layer base, components; объявляет слои и их порядок, а правила кладутся внутрь @layer имя { … }. Слои судья сравнивает раньше специфичности: побеждает слой, объявленный позже, каким бы слабым ни был его селектор.
Здесь правило с id лежит в раннем слое base, а правило для тега — в позднем components. Обратите внимание: в коде слой base заполнен ниже, но порядок задаёт первая строка. Какого цвета абзац?
p (0, 0, 1) из позднего слоя победил #lead (1, 0, 0) из раннего — специфичность правил из разных слоёв даже не сравнивается. А правила, не положенные ни в один слой, сильнее любого слоя: ваш обычный CSS остаётся главным, даже если библиотеку упаковали в слой. У объявлений с !important порядок обратный: ранний слой сильнее позднего, а любой слой сильнее стилей вне слоёв. Слои — Baseline widely с 14.09.2024. В примерах пролога они больше не встретятся, а в чужом CSS их узнают по @layer.Судья разбирает спор только между объявлениями, нацеленными на сам элемент. Если таких нет вовсе, свойство либо приходит от родителя — наследуется, — либо берёт начальное значение. Наследуются в основном свойства текста: color, font-size, font-family, line-height. Свойства коробки — border, padding, margin — нет: иначе рамка родителя повторялась бы вокруг каждого слова внутри.
У div заданы цвет, размер шрифта и рамка, внутри — ссылка, кнопка и простой span. Последний вызов сравнивает свойство у всех найденных элементов и отвечает «одинаково» или «по-разному», не называя чисел: так короче, когда важно само совпадение. Предскажите три ответа: цвет ссылки и span, толщину верхней рамки у span и одинаков ли размер шрифта у span и кнопки.
span получил от родителя зелёный цвет, а рамку — нет: 0px. Ссылка осталась синей — rgb(0, 0, 238), — хотя зелёный родителя ей был доступен. Дело в стилях браузера: у него свой лист правил, и ссылкам там назначен свой цвет. Любое объявление, нацеленное на элемент, сильнее наследства — даже объявление браузера. Кнопке браузер так же назначает свой шрифт, поэтому 20px родителя до неё не доходят. Шрифт кнопки в Chrome и Firefox чуть различается, поэтому same сверяет его со шрифтом span: «по-разному».Стили браузера сильнее наследства, но слабее любого вашего правила: даже * { color: … } перекрасит ссылку. А если нужно именно значение родителя, его просят словом inherit: объявление color: inherit нацелено на сам элемент и поэтому спорит по общим правилам.
Соберём всё в один список. Браузер идёт по нему сверху вниз и останавливается на первом вопросе, где спорящие объявления различаются, — до остальных дело не доходит:
| Вопрос судьи | Кто выигрывает |
|---|---|
| 1. Важное или обычное? | объявление с !important; среди обычных ваши стили сильнее стилей браузера |
2. Из атрибута style? | атрибут — против любого правила из <style> |
| 3. В каком слое? | слой, объявленный позже; правило вне слоёв — против любого слоя; у !important — наоборот |
| 4. Какая специфичность? | больше id, при равенстве — больше классов, затем больше тегов; :where() весит ноль |
| 5. Кто ниже в коде? | последнее объявление |
Если на элемент не нацелено ни одного объявления, судье нечего решать: цвет и шрифт придут от родителя, а у рамки останется начальное значение — её не будет. Именно поэтому страница без единого стиля всё равно читается: текст наследует шрифт, а всё остальное берёт начальные значения.
Настоящий судья с этим списком живёт в инструментах разработчика. Откройте их клавишей F12 (в macOS — Cmd+Option+I) и выберите элемент: в блоке Styles проигравшие объявления перечёркнуты, а рядом видно, из какого правила они пришли. Прибор так не умеет, это ручной шаг, и подробно панель разобрана в части II — вкладка Elements и блок Styles.
- Селектор — адрес элементов: тег
li, класс.done, id#menu, атрибут[data-id]. Пробел ищет потомков на любой глубине,>— только прямых детей, а склейка без пробелаli.doneтребует всего сразу от одного элемента. - Спор за свойство решает судья, и вопросы он задаёт по порядку: важность, атрибут
style, слой, специфичность, порядок в коде. Решает первое же различие. - Специфичность — три отдельных счётчика: id, затем классы с атрибутами и псевдоклассами, затем теги. Сравнивают их слева направо;
*, пробел и>не весят ничего, а:where()обнуляет вес всего, что внутри. - Слой, объявленный позже, сильнее раннего при любой специфичности, а правила вне слоёв сильнее любого слоя; у
!importantпорядок обратный. - Наследуются свойства текста — цвет и шрифт, а рамка и отступы нет. Любое объявление для самого элемента, даже из стилей браузера, сильнее наследства: поэтому ссылка не берёт цвет родителя без
color: inherit. - Проигравшие объявления видны в инструментах разработчика: блок Styles показывает их перечёркнутыми.
1/4Какая специфичность у селектора ul#menu > li.done:first-child?
2/4У абзаца атрибут style="color: blue", а в <style> стоит p { color: red !important; }. Какого цвета абзац?
3/4У div заданы color и border, внутри лежит span без своих стилей. Что он получит от родителя?
4/4Правило :where(#box) .note стоит в коде выше, .note — ниже. Оба задевают один элемент. Чей цвет он получит?
Адреса для меню
Сейчас count("a") находит все три ссылки меню. Замените его пятью вызовами count и подберите селекторы: активная ссылка — 1; ссылка на блог по её атрибуту href — 1; ссылка внутри .more — 1; первая ссылка меню, найденная через :first-child, — 1; ссылки, которые лежат прямо в nav, но не глубже, — 2.
Ловушка — четвёртый пункт. Селектор a:first-child без #top > найдёт две ссылки: «О нас» тоже первый ребёнок, только своего div. Ограничение #top > оставляет лишь прямых детей меню. В пятом пункте пробел вместо > вернул бы все три ссылки.
Перекрасьте, не трогая чужого
Кнопка должна быть зелёной, но правило .btn.primary проигрывает. Сначала объясните почему. Потом допишите в конец <style> одно правило, чтобы кнопка стала зелёной. Существующие правила не менять, !important и атрибут style не использовать.
#promo .btn весит (1, 1, 0), .btn.primary — (0, 2, 0): id решил спор на первом счётчике. Новому правилу тоже нужен id. Первый вариант:
Это (1, 2, 0), сильнее по второму счётчику. Второй вариант:
Здесь (1, 1, 0) — ровно столько же, сколько у соперника, и правило выигрывает только тем, что стоит ниже. В обоих случаях прибор напечатает .btn → color: rgb(0, 128, 0). Первый вариант надёжнее: он не сломается, если кто-нибудь переставит правила.
Ссылка в цвет заметки
Ссылка внутри заметки должна быть того же зелёного цвета, что и заметка, а ссылка в обычном абзаце — остаться синей. Допишите одно правило в конец <style>. Зелёный цвет второй раз не пишите, !important не используйте.
Прибор напечатает a → color: rgb(0, 0, 255), rgb(0, 128, 0). Слово inherit берёт цвет у родителя — у заметки. Короткое .note a { color: inherit; } не сработает: его (0, 1, 1) проигрывает #post a с его (1, 0, 1), и обе ссылки останутся синими. Наследство, которое попросили явно, спорит по общим правилам, как любое объявление.