CSS-псевдоклассы: замена JS-обработчикам событий
CSS Deep-Dive · 2026

CSS-состояния vs JavaScript-события:
Когда псевдоклассы заменяют обработчики

CSS незаметно накапливает псевдоклассы, которые обрабатывают взаимодействия, ранее требовавшие JavaScript. Обзор современной модели состояний CSS и её сравнение с JavaScript EventTarget API.

Олег Максимов 5 июля 2026 11 мин чтения

Введение: размытие границы

CSS слушает нас. Нет, не в буквальном смысле. Речь о том, что CSS накапливает всё больше псевдоклассов, которые позволяют реагировать на действия пользователя без единой строки JavaScript. Граница между тем, что CSS обрабатывает декларативно, и тем, что требует императивного JavaScript, смещается — и смещается уже давно.

Псевдоклассы отслеживают состояния, а не события. Это важное различие: событие происходит в конкретный момент времени, а состояние существует на протяжении некоторого периода. :hover охватывает весь промежуток между pointerenter и pointerleave. Однако на практике CSS-псевдокласс ощущается как обработчик события — и всё чаще CSS берёт на себя логику, для которой раньше требовался JavaScript.

В этом руководстве я разберу каждый CSS-псевдокласс, который действует как обработчик событий: классические состояния наведения/фокуса, реляционные селекторы :focus-within и :has(), состояния валидации форм, состояния медиаэлементов, а также экспериментальное предложение event-trigger, которое дало бы CSS настоящие возможности прослушивания событий.

Классические интерактивные псевдоклассы

:hover и :active

Самые известные CSS-состояния взаимодействия. :hover активируется, когда курсор входит в область элемента, и деактивируется при выходе. Это соответствует JavaScript-событиям pointerenter и pointerleave.

:active применяется, пока элемент нажат — между pointerdown и pointerup/pointercancel. Оба — чистые трекеры состояний:

/* CSS — декларативное отслеживание состояния */
button:hover {
  background: #e0f2fe;
  transform: scale(1.02);
  transition: transform 0.15s ease;
}

button:active {
  transform: scale(0.98);
}

Сравните с JavaScript-аналогом:

// JavaScript — императивное отслеживание событий
button.addEventListener('pointerenter', () => {
  button.style.background = '#e0f2fe';
  button.style.transform = 'scale(1.02)';
});
button.addEventListener('pointerleave', () => {
  button.style.background = '';
  button.style.transform = '';
});

CSS-версия короче, декларативна и использует аппаратное ускорение композитинга. JS-версия даёт больше контроля — можно условно применять эффекты, запускать побочные действия или координировать несколько элементов — но для простых эффектов наведения CSS объективно лучше.

:focus и :focus-visible

:focus применяется, когда элемент получает фокус (через клавиатуру, клик или element.focus()). Это CSS-аналог JavaScript-событий focus и blur.

:focus-visible интереснее. Браузер применяет его, когда :focus срабатывает, но дополнительно использует эвристики для определения, нужно ли показать видимый индикатор фокуса. Это способ браузера отличить навигацию с клавиатуры от кликов мышью — различие, которое удивительно сложно реализовать в JavaScript:

/* CSS — автоматическое определение клавиатура/мышь */
button:focus-visible {
  outline: 2px solid #0f766e;
  outline-offset: 2px;
}

button:focus:not(:focus-visible) {
  outline: none; /* Клик мышью — скрываем кольцо */
}

Для воспроизведения этого в JavaScript пришлось бы запрашивать сам псевдокласс через element.matches(':focus-visible'). CSS обрабатывает это нативно.

:focus-within и :has()

Здесь CSS действительно начинает вести себя как система событий. :focus-within применяется к родительскому элементу, когда любой из его потомков в фокусе. По сути, это всплытие события фокуса на уровне CSS.

:has() ещё мощнее — это реляционный селектор, который срабатывает, если потомок соответствует заданному селектору. Эти два селектора делают одно и то же:

/* Оба выделяют форму, когда дочерний input в фокусе */
form:focus-within {
  border-color: #0f766e;
  box-shadow: 0 0 0 3px rgba(15, 118, 110, 0.2);
}

form:has(:focus) {
  border-color: #0f766e;
  box-shadow: 0 0 0 3px rgba(15, 118, 110, 0.2);
}

Селектор :has() выходит далеко за рамки обработки фокуса — он может обнаруживать любое состояние потомка, становясь настоящим условным оператором CSS. Например, .card:has(.badge) изменяет раскладку карточки, если она содержит значок, или form:has(:invalid) показывает баннер ошибки формы без JavaScript.

Псевдоклассы состояния форм

:checked

Псевдокласс :checked применяется к чекбоксам, радио-кнопкам и элементам <option> в <select>, когда они выбраны. JavaScript-аналог — событие change:

/* CSS */
.toggle:checked + .toggle-label {
  background: #0f766e;
}

.toggle:checked ~ .menu {
  display: block;
}
// JavaScript
checkbox.addEventListener('change', (event) => {
  if (event.target.checked) {
    menu.style.display = 'block';
  } else {
    menu.style.display = 'none';
  }
});

Паттерн :checked + комбинаторы соседних элементов — основа CSS-компонентов без JavaScript: выпадающих меню, аккордеонов, вкладок и переключателей тёмной темы.

:valid, :invalid, :user-valid, :user-invalid

HTML-валидация форм имеет встроенное отслеживание состояний в CSS. Псевдоклассы :valid и :invalid применяются на основе объекта ValidityState — той же валидации, которую браузер использует для нативного UI.

Однако :valid и :invalid срабатывают сразу при загрузке страницы. Это означает, что пустое обязательное поле показывается как :invalid до того, как пользователь к нему прикоснулся. Псевдоклассы :user-valid и :user-invalid решают эту проблему — они срабатывают только после реального взаимодействия пользователя с полем:

/* Показываем ошибки только после взаимодействия */
input:user-invalid {
  border-color: #dc2626;
  background: #fef2f2;
}

input:user-valid {
  border-color: #16a34a;
  background: #f0fdf4;
}

/* Подсветка автозаполненных значений */
input:autofill {
  border-color: #16a34a;
}

Псевдокласс :autofill — ещё одна победа CSS. В JavaScript нет чистого способа отследить автозаполнение поля браузером. :autofill обрабатывает это нативно.

Для базовой стилизации валидации CSS не требует JavaScript вообще. Для нестандартного поведения (свои сообщения об ошибках, асинхронная валидация, проверки на сервере) JavaScript всё ещё необходим.

Псевдоклассы медиаэлементов

Одно из новейших дополнений CSS — и наглядная демонстрация того, как CSS приобретает возможности, похожие на события — это набор псевдоклассов медиаэлементов для <audio> и <video>.

Они входят в Interop 2026 и активно внедряются браузерами. Firefox уже добавил поддержку; Chrome работает над реализацией.

CSS-псевдокласс Аналог JavaScript-события Описание
:buffering waiting Медиа буферизирует данные
:muted volumechange + .muted Звук отключён
:paused pause Воспроизведение приостановлено
:playing playing (не play) Активное воспроизведение
:seeking seeking Пользователь перематывает
:stalled stalled Данные неожиданно недоступны
:volume-locked Прямого аналога нет Регулировка громкости заблокирована ОС

Пример: показываем индикатор загрузки только когда видео активно буферизируется:

video:buffering + .spinner {
  display: block;
}

video:playing + .spinner {
  display: none;
}

Псевдокласс :volume-locked особенно примечателен — в JavaScript нет простого способа определить, заблокирована ли громкость на уровне ОС. Ближайший JS-подход требует создания тестового элемента и попытки изменить его громкость — это хрупко и имеет побочные эффекты. CSS обрабатывает это как нативный запрос состояния.

Состояния диалогов, поповеров и полноэкранного режима

:popover-open, :open, :modal

Нативные HTML-элементы вроде <dialog>, поповеров (атрибут popover) и <details> имеют встроенные псевдоклассы состояния, устраняющие необходимость в JavaScript для управления визуальным состоянием:

/* Стилизуем поповер в открытом состоянии */
[popover]:popover-open {
  opacity: 1;
  transform: scale(1);
}

/* Стилизуем открытый 
*/ details:open summary { font-weight: 600; } /* Стилизуем фон модального диалога */ dialog:modal::backdrop { background: rgba(0, 0, 0, 0.5); backdrop-filter: blur(4px); }

В JavaScript отслеживание этих состояний требует прослушивания события toggle и проверки свойств open или modal:

// JavaScript-аналог
dialog.addEventListener('toggle', () => {
  if (dialog.open) {
    backdrop.style.opacity = '1';
  } else {
    backdrop.style.opacity = '0';
  }
});

:fullscreen

Псевдокласс :fullscreen применяется к элементу, отображаемому в полноэкранном режиме. Его JavaScript-аналог — событие fullscreenchange с проверкой document.fullscreenElement:

/* CSS */
#player:fullscreen {
  background: #000;
  padding: 0;
}
// JavaScript
document.addEventListener('fullscreenchange', () => {
  if (document.fullscreenElement) {
    player.style.background = '#000';
  } else {
    player.style.background = '';
  }
});

Состояния на основе URL

:target

Когда хеш URL (например, #section-3) совпадает с id элемента, этот элемент соответствует псевдоклассу :target. Это позволяет реализовать подсветку навигации и анимации появления секций без JavaScript:

section {
  opacity: 0.3;
  transition: opacity 0.4s ease;
}

section:target {
  opacity: 1;
  scroll-margin-top: 5rem;
}

С JavaScript пришлось бы слушать событие hashchange и вручную переключать класс на целевом элементе. CSS делает это декларативно, с плавными переходами на потоке композитинга браузера.

Будущее: event-trigger

Спецификация Animation Triggers (CSS Level 5) предлагает event-trigger — механизм, который дал бы CSS настоящие возможности прослушивания событий. Пока не поддерживается ни одним браузером, но представляет логическую конечную точку описанного в этой статье тренда.

Идея проста: элемент объявляет именованный триггер события, другой элемент подписывается на него, и когда событие срабатывает, запускается анимация. Предлагаемый синтаксис:

@keyframes fade-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

/* Кнопка запускает --event по клику */
button {
  event-trigger: --event click;
}

/* Div играет fade-in при срабатывании --event */
div {
  animation-trigger: --event play-forwards;
  animation: fade-in 300ms both;
}

event-trigger поддерживает stateless события (вроде click — нельзя отменить клик) и stateful события (вроде interest — интерес можно потерять). Для stateful событий используется синтаксис прямого/обратного действия:

@keyframes fade-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

button {
  event-trigger: --event interest / interest;
}

div {
  animation-trigger: --event play-forwards play-backwards;
  animation: fade-in 300ms both;
}

Ключевое слово interest относится к грядущему Interest Invoker API. Другие источники событий: click, dblclick, touch, activate и keypress().

Допустимые действия анимации: none, play, play-once, play-forwards, play-backwards, pause, reset и replay.

Если W3C продвинет эту спецификацию (а она упоминает возможность всплытия событий), мы можем увидеть мир, где клик по дочернему элементу запускает анимацию на совершенно не связанном родителе — всё в CSS, без JavaScript.

Фреймворк принятия решений: CSS или JavaScript для взаимодействий

Со всеми этими новыми возможностями, когда стоит использовать CSS, а когда — JavaScript? Ответ не в черно-белых тонах — у обоих подходов есть своё место.

👍 Используйте CSS-псевдоклассы

  • Визуальные эффекты при наведении, нажатии, фокусе
  • Стили валидации форм (:user-valid)
  • Распространение состояния родитель-потомок (:focus-within, :has())
  • CSS-компоненты-переключатели (чекбокс-хак)
  • Визуальные состояния медиаплеера
  • Стили открытия/закрытия диалогов и поповеров
  • Подсветка секций по хешу URL

👎 Оставьте JavaScript

  • Побочные эффекты (API-запросы, localStorage, аналитика)
  • Многошаговые взаимодействия (мастера, оформление заказа)
  • Drag-and-drop, кастомные жесты
  • Горячие клавиши
  • Координация между компонентами (управление состоянием)
  • Асинхронная валидация (проверки на сервере)
  • Анимации со сложной синхронизацией

Хорошее практическое правило: если взаимодействие чисто визуальное — меняет внешний вид в ответ на изменение состояния — используйте CSS. Если взаимодействие должно что-то сделать (загрузить данные, записать в хранилище, скоординировать несколько компонентов) — используйте JavaScript.

CSS не пытается заменить JavaScript полностью. Он вытесняет те части JavaScript, которые никогда не были сильной стороной: простое управление состояниями UI, визуальную обратную связь и переключение классов в DOM. Для эволюции веба это здоровое разделение. CSS отвечает за внешний вид; JavaScript — за логику. И по мере того, как CSS получает всё больше возможностей для отслеживания состояний, граница продолжает смещаться — давая разработчикам больше выбора и более простой код.

Для более широкого взгляда на то, как CSS поглощает территорию JavaScript, см. мою статью о современных CSS-паттернах, заменяющих JavaScript, где рассматриваются CSS if(), style queries и расширенный attr(). Также ознакомьтесь с новыми CSS-возможностями Chrome 150 для последних реализаций в браузерах.

Часто задаваемые вопросы

Могут ли CSS-псевдоклассы полностью заменить JavaScript-обработчики событий?
Нет. CSS-псевдоклассы отлично справляются с состояниями (наведение, фокус, чекбоксы, стили валидации), но не могут заменить сложную обработку событий: drag-and-drop, горячие клавиши, кастомные жесты или многошаговые взаимодействия с побочными эффектами. CSS — декларативен, он описывает, как выглядит элемент в заданном состоянии. JavaScript — императивен, он управляет последовательностями действий. Используйте CSS для визуальных взаимодействий и JavaScript для поведенческой логики.
Какие CSS-псевдоклассы аналогичны JavaScript-событиям?
Многие CSS-псевдоклассы соответствуют JavaScript-событиям: :hover (pointerenter/pointerleave), :active (pointerdown/pointerup), :focus (focus/blur), :checked (change/input), :valid/:invalid (события валидации форм), :focus-within (всплытие фокуса к родителю), :fullscreen (fullscreenchange), :target (hashchange), а также псевдоклассы медиаэлементов :playing, :paused, :buffering, :muted, :seeking, :stalled.
Что такое CSS event-trigger и поддерживается ли он?
event-trigger — это предложенная функция CSS в спецификации Animation Triggers (CSS Level 5). Она позволит CSS слушать JavaScript-события (click, interest, keypress и др.) и запускать анимации в ответ. На данный момент не поддерживается ни одним браузером. Синтаксис использует свойства event-trigger-name и event-trigger-source или сокращение event-trigger. Например: button { event-trigger: --event click; } div { animation-trigger: --event play-forwards; }.
Когда использовать CSS-псевдоклассы вместо JavaScript?
Используйте CSS-псевдоклассы когда: (1) взаимодействие чисто визуальное — эффекты наведения, индикаторы фокуса, стили валидации; (2) взаимодействие включает связь родитель-потомок (focus-within, :has()); (3) нужно стилизовать нативные элементы форм на основе состояния; (4) нужно стилизовать медиаэлементы на основе состояния воспроизведения; (5) хотите избежать накладных расходов JavaScript для простых переходов состояний UI. Используйте JavaScript когда: нужны побочные эффекты (API-запросы, localStorage), многошаговые взаимодействия, drag-and-drop, кастомные жесты, горячие клавиши или координация между элементами вне связи родитель-потомок.
Чем :user-valid и :user-invalid отличаются от :valid и :invalid?
:user-valid и :user-invalid срабатывают только после того, как пользователь реально взаимодействовал с полем и убрал фокус (blur). В отличие от :valid/:invalid, которые применяются сразу при загрузке страницы (показывая каждое обязательное поле как invalid до того, как пользователь его коснулся), варианты с :user-* ждут реального взаимодействия. Это предотвращает показ ошибок валидации до того, как пользователь успел заполнить форму.
Поддерживаются ли псевдоклассы медиаэлементов (:playing, :paused) в браузерах?
Псевдоклассы медиаэлементов относительно новы, но активно внедряются. Firefox уже добавил поддержку, и они входят в Interop 2026, что означает активную работу Chrome и других браузеров над реализацией. Поддерживаемые псевдоклассы: :buffering, :muted, :paused, :playing, :seeking, :stalled и :volume-locked. Проверяйте caniuse.com для актуального статуса.
В чем разница между :focus-within и :has(:focus)?
Оба селектора дают одинаковый визуальный результат: применяют стили к родителю, когда дочерний элемент в фокусе. form:focus-within срабатывает, когда любой фокусируемый элемент внутри формы получает фокус. form:has(:focus) делает то же самое через реляционный селектор :has(). Разница в том, что :focus-within — это специальный селектор с более широкой поддержкой браузеров, а :has() — более общий реляционный селектор, который может соответствовать любым отношениям предок-потомок.
Контакты

Обсудим ваш проект

Создаёте веб-приложение с современным CSS? Я помогу спроектировать чистую, поддерживаемую архитектуру, использующую лучшее из CSS и JavaScript.