Псевдоклассы и псевдоэлементы в CSS: короткое руководство
Двоеточие или два двоеточия? Разбираемся, чем :hover отличается от ::before, зачем нужны :is() и :has() и где браузеры до сих пор ведут себя по-разному.

Псевдоклассы и псевдоэлементы делают в CSS то, ради чего иначе пришлось бы дописывать классы в разметку или тянуть JavaScript: реагируют на состояние элемента и рисуют то, чего в HTML нет. Путаница начинается с синтаксиса — :hover с одним двоеточием и ::before с двумя, — а продолжается на новых селекторах вроде :has(), которые в стабильных браузерах появились только в 2023 году. Разберём, что есть что, где граница между ними и на что смотреть в поддержке. Ориентируемся на актуальные версии Chrome, Firefox и Safari 2024 года.
Псевдокласс и псевдоэлемент — в чём разница
Псевдокласс выбирает элемент, который уже есть в документе, но находится в определённом состоянии: на него навели курсор, он первый среди соседей, поле заполнено неверно. Синтаксис — одно двоеточие: a:hover, li:first-child, input:invalid.
Псевдоэлемент создаёт или адресует часть элемента, которой нет в дереве DOM: сгенерированный контент до и после, первая строка абзаца, маркер списка. Синтаксис по современному стандарту — два двоеточия: p::first-line, li::marker, div::before.
DOM (Document Object Model) — представление HTML-страницы в виде дерева объектов, с которым работают браузер и JavaScript. Псевдоэлементы в этом дереве отдельными узлами не появляются, поэтому получить ::before через querySelector нельзя.
Почему у псевдоэлементов два двоеточия
В CSS2 и псевдоклассы, и псевдоэлементы писали с одним двоеточием: :before, :first-line. В CSS3 псевдоэлементам дали два двоеточия, чтобы визуально отделить их от псевдоклассов. Старый синтаксис с одним двоеточием браузеры до сих пор понимают для четвёрки ::before, ::after, ::first-line, ::first-letter — ради обратной совместимости. Для всего, что появилось позже (например, ::marker, ::selection), одно двоеточие уже не работает. Пишите два — это правильно и работает везде.
Псевдоклассы, которые нужны каждый день
Их десятки, но в реальной вёрстке крутится небольшой набор.
- Состояния ссылок и кнопок:
:hover,:focus,:active,:visited. Для доступности с клавиатуры отдельно смотрите на:focus-visible— он показывает обводку только при навигации с клавиатуры, а не по клику мышью. - Позиция среди соседей:
:first-child,:last-child,:nth-child(),:only-child. Не путайте:nth-child(2)(второй ребёнок родителя вообще) и:nth-of-type(2)(второй элемент такого же тега). - Формы:
:checked,:disabled,:required,:valid,:invalid,:placeholder-shown. На них строятся кастомные чекбоксы и подсветка ошибок без JavaScript. - Логические:
:not(),:is(),:where(),:has(). О них ниже отдельно.
:nth-child — где чаще всего ошибаются
Формула an+b отсчитывается от 1, а не от 0. Полосатая таблица через чётные строки:
/* Каждая чётная строка таблицы */
tbody tr:nth-child(even) {
background: #f5f5f5;
}
/* Каждый третий элемент, начиная с первого: 1, 4, 7... */
li:nth-child(3n + 1) {
font-weight: bold;
}
Проверка: если после применения полосы съехали на одну строку, вы, скорее всего, спутали even и odd либо забыли, что счёт идёт с единицы.
:is(), :where() и :has() — новая логика
Эта тройка меняет то, как пишут сложные селекторы. Разберём по одному.
:is() сокращает перечисления. Вместо трёх строк с одинаковыми правилами:
/* Было */
article h1,
article h2,
article h3 {
line-height: 1.2;
}
/* Стало */
article :is(h1, h2, h3) {
line-height: 1.2;
}
:where() делает то же самое, но с одним отличием: его специфичность всегда равна нулю. Это удобно для базовых стилей, которые потом легко переопределить любым обычным селектором. :is(), наоборот, берёт специфичность самого «весомого» селектора в скобках.
:has() — так называемый родительский селектор, которого CSS ждал больше десяти лет. Он выбирает элемент по тому, что находится внутри него:
/* Карточка, внутри которой есть картинка */
.card:has(img) {
padding-top: 0;
}
/* Форма, в которой есть невалидное поле */
form:has(input:invalid) button[type="submit"] {
opacity: 0.5;
}
До появления :has() «покрасить родителя по состоянию ребёнка» без JavaScript было невозможно в принципе. Это не синтаксический сахар, а новая категория задач, решаемых на чистом CSS.
Поддержка: где осторожнее
:has() стал доступен в стабильных Chrome и Safari в 2023 году, в Firefox поддержка появилась позже. Если проект должен работать в старых браузерах — проверьте конкретную ситуацию на caniuse.com и держите запасной вариант через классы. :is() и :where() поддерживаются шире и дольше.
Псевдоэлементы: что можно нарисовать
| Псевдоэлемент | Что адресует | Типичное применение |
|---|---|---|
::before / ::after | Вставленный контент до/после содержимого | Иконки, декор, счётчики, очистка float |
::first-line | Первая строка блока текста | Типографика, буквица без буквицы |
::first-letter | Первая буква блока | Буквица в начале статьи |
::marker | Маркер или номер списка | Кастомные маркеры <li> |
::selection | Выделенный пользователем текст | Цвет выделения мышью |
::placeholder | Текст-подсказка в поле | Стилизация placeholder |
Главное правило ::before и ::after
Без свойства content они не отображаются вообще. Даже если контента нет, но элемент нужен как декоративный блок, пишут пустую строку:
.badge::after {
content: ""; /* обязательно, иначе элемента не будет */
display: block;
width: 8px;
height: 8px;
border-radius: 50%;
background: #e00;
}
Второй момент: ::before и ::after не работают на замещаемых элементах вроде <img>, <input>, <br> — у них нет содержимого, внутрь которого можно что-то вставить.
Где обычно всё ломается
- Один двоеточие вместо двух на новом псевдоэлементе.
:markerмолча не сработает, а::markerсработает. Ошибки в консоли не будет — стиль просто не применится. - Забытый
content. Самая частая причина «почему мой ::after не виден». - Ожидание, что
:has()есть везде. В старом WebView или корпоративном браузере правило проигнорируется, и вёрстка поедет без предупреждения. - Путаница специфичности
:is()и:where(). Если базовые стили внезапно перебивают ваши, проверьте, не завёрнуты ли селекторы в:is()с высокой специфичностью. - Попытка навесить
::beforeна<input>. Не выйдет — используйте обёртку или соседний элемент.
Что делать дальше
Для проверки поддержки любого селектора в целевых браузерах держите под рукой caniuse.com — это быстрее, чем ловить баги на устройствах. Если пишете компоненты, которые должны переопределяться темами, заворачивайте базовые правила в :where(), чтобы не воевать со специфичностью. А прежде чем тянуть в проект JavaScript ради реакции на состояние формы или наличие дочернего элемента, проверьте, не решается ли задача связкой :has() и :invalid — за последние два года граница возможного в CSS заметно сдвинулась.
Инфографика для маркетплейса: Три главных преимущества товара короткими фразами, выноски в безопасной зоне превью, точный размер площадки.
Спросить за 60 ₽Источники: MDN: Псевдоклассы, MDN: Псевдоэлементы
Частые вопросы
Что делать, если :has() не работает в моём браузере?
Сколько двоеточий писать — одно или два?
Почему мой ::after не отображается?
Чем :is() отличается от :where()?
Можно ли получить псевдоэлемент через JavaScript?
В чём разница между :nth-child и :nth-of-type?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.