:has() + scroll-driven-анимации — реактивные хедеры без JSСвязка селектора
:has() и scroll-driven анимаций позволяет управлять состоянием UI-элементов по скроллу без единой строчки JavaScript. Раньше для sticky-хедеров, меняющих тень по скроллу, или сайдбаров с плавающим появлением тащили Intersection Observer или слушали scroll — теперь это решается чисто CSS. Основная ошибка: верстальщики продолжают использовать тяжелые JS-обвязки для задач, которые браузер уже умеет в потоке композитинга.Как работает связка
Создаешь невидимый маркер внутри контента — обычный div с scroll-driven анимацией прозрачности. Затем на контейнере через
:has() проверяешь состояние этого маркера: если он анимировался (значение opacity изменилось), значит, скролл прошел заданный порог. Пример для сайдбара:.marker {
animation: markerAnim linear;
animation-timeline: scroll();
}
@keyframes markerAnim {
0% { opacity: 0; }
100% { opacity: 1; }
}
body:has(.marker[style*="opacity: 1"]) .sidebar {
transform: translateX(0);
box-shadow: 2px 0 10px rgba(0,0,0,0.2);
}Маркер не занимает места, не ломает layout, а анимация выполняется в отдельном потоке — без пересчета layout и repaint. Подходит для динамических хедеров с blur-эффектом, сайдбаров, floating-кнопок и control-панелей в дашбордах.
Production-контекст и trade-offs
Реальный пример: в админке e-commerce при скролле таблицы заказов нужно показывать панель фильтров, которая прилипает к верху и скрывает лишние контролы. Через
:has() + scroll() это делается декоративным маркером на первом скролле страницы. Важно: :has() внутри стилей для ключевых элементов (body, main) может редко, но снижать производительность в больших документах с тысячами DOM-узлов. Практический совет: используй маркер с position: absolute и нулевыми размерами, чтобы избежать дополнительного reflow.Ошибка: состояние через класс, а не через свойство
Типичная ошибка — пытаться проверять класс маркера, который не меняется при scroll-driven анимациях. Классы изменяются только через JS или через CSS-анимации с
animation-fill-mode: forwards и интерполяцией opacity. Всегда проверяй через [style*="..."] или используй CSS-хуки через @property для кастомных свойств. Для старых браузеров без поддержки :has() оставляй Intersection Observer как фолбэк через @supports not selector(:has()).Вывод: CSS
:has() с scroll-driven анимациями — инженерный компромисс между производительностью и браузерной поддержкой, который уже сегодня решает задачи реактивных интерфейсов без JS.