← назад к разделу

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

База и комбинаторы

button { }          /* по тегу */
.card { }           /* по классу — рабочая лошадка */
#app { }            /* по id — в стилях лучше не использовать */
[disabled] { }      /* по атрибуту */
[href^="https"] { } /* атрибут начинается с... (еще: $= кончается, *= содержит) */

Комбинаторы соединяют селекторы отношениями в дереве:

.card p { }      /* пробел: потомок на ЛЮБОЙ глубине */
.card > p { }    /* прямой ребёнок */
h2 + p { }       /* сосед сразу после h2 */
h2 ~ p { }       /* все последующие соседи */

Разница «потомок vs ребёнок» стреляет на вложенных структурах: .menu li покрасит и пункты вложенного подменю, .menu > li — только верхний уровень. А соседний комбинатор + — идиома «отступ между, но не снаружи»: p + p { margin-top: 1em } даёт интервал только между абзацами.

Список через запятую — это несколько независимых селекторов с одним телом: h1, h2, h3 { }. Ошибка в одном селекторе списка отменяет всё правило целиком — об этом ниже, у :is() есть лекарство.

Псевдоклассы: состояния и позиции

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

Интерактивные состояния:

a:hover { }          /* курсор над элементом */
button:active { }    /* в момент нажатия */
input:focus { }      /* элемент в фокусе */
button:focus-visible { } /* фокус С КЛАВИАТУРЫ — рамка табающим, без рамки кликающим */
input:disabled { }
input:checked { }    /* чекбокс/радио отмечен */

Пара :focus / :focus-visible — важная деталь доступности: убирать outline совсем нельзя (клавиатурные пользователи теряют место фокуса), а :focus-visible показывает индикатор только тем, кто ходит клавиатурой.

Счёт детей:

li:first-child { }
li:last-child { }
li:nth-child(2) { }      /* второй ребёнок */
li:nth-child(odd) { }    /* нечётные — зебра в таблицах */
li:nth-child(3n) { }     /* каждый третий */
li:nth-child(n + 4) { }  /* с четвёртого и дальше */

Классические грабли — разница nth-child и nth-of-type. p:nth-child(2) означает «элемент, который является вторым ребёнком своего родителя И при этом параграф». Если первым ребёнком стоит заголовок, вторым — параграф, правило сработает; но p:nth-child(1) не найдёт ничего. p:nth-of-type(2) считает только параграфы: второй параграф среди параграфов. Не сработал nth-child — почти всегда в дереве есть «лишний» элемент, сбивающий счёт.

Отрицание: button:not(.primary) — все кнопки, кроме основной; li:not(:last-child) — «всем, кроме последнего» (тот же приём межэлементных отступов).

Псевдоэлементы: контент из ничего

Псевдоэлемент (двойное двоеточие) — не выбор существующего, а создание виртуального элемента:

.required::after {
  content: " *";
  color: red;
}
.quote::before { content: "« "; }
.intro::first-line { font-weight: 600; }
::selection { background: #ffe08a; } /* выделение текста */

::before/::after вставляют по виртуальному ребёнку в начало и конец элемента. Обязательное условие — свойство content (хоть пустое content: ""), без него псевдоэлемент не существует. Пустые before/after с фоном и позиционированием — стандартная техника декора: линии, иконки, оверлеи — без мусорных <div> в разметке. Ограничение: у элементов без внутреннего содержимого (<img>, <input>) они не работают.

Современный слой: :is, :where, :has

:is() сворачивает повторы и «прощает» ошибки внутри списка:

/* вместо трёх селекторов */
:is(h1, h2, h3) > a { color: inherit; }

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

:has() — долгожданный «родительский селектор»: выбирает элемент по тому, что у него внутри:

.card:has(img) { padding: 0; }            /* карточка с картинкой */
label:has(input:checked) { font-weight: 600; } /* подпись отмеченного чекбокса */
form:has(:invalid) button[type=submit] { opacity: .5; }

До :has() такие вещи требовали JavaScript. Поддержка во всех современных браузерах уже есть — но в проектах со старыми браузерами проверяйте по caniuse.

Коротко

  • Повседневная база: класс + четыре комбинатора (, >, +, ~). p + p — идиома отступов «между».
  • Псевдоклассы — состояния и позиции: :hover, :focus-visible (доступный фокус), :checked, :nth-child.
  • nth-child считает всех детей, nth-of-type — только элементы своего типа. Сбился счёт — в дереве лишний элемент.
  • ::before/::after создают виртуальные элементы; без content не существуют; на <img>/<input> не работают.
  • :is() сворачивает списки, :where() — то же с нулевой специфичностью, :has() — селектор «по содержимому».

Что почитать дальше

  • Каскад и специфичность — сколько «весит» каждый из этих селекторов.
  • Блочная модель — следующая тема цикла.
  • События DOM — closest() и делегирование: те же селекторы на стороне JavaScript.