Дайджесты новостей
Демонстрация предотвращения коллизии в CSS с помощью плавающего якоря, блока измерения зазора и реактивного скрытия перекрывающего элемента.

Чистый CSS против наезжающего декора: связка Anchor Positioning и Scroll-Driven Animations

Создание сложных адаптивных интерфейсов всегда балансирует между эстетикой и функциональностью. Дизайнеры любят оживлять верстку плавающими декоративными элементами: абстрактными геометрическими фигурами, фоновыми паттернами, стрелками и бейджами. Однако при уменьшении ширины экрана или динамическом изменении размера контейнера этот декор начинает наезжать на заголовки и текст, превращая читабельную статью в хаос.

Классическое решение проблемы — использование медиавыражений (@media (max-width: 900px)). Но медиазапросы привязаны к физической ширине вьюпорта браузера, а не к фактическому расстоянию между конкретными блоками на странице. В многоколоночных сетках декор может перекрыть текст даже на широком 4K-мониторе, если соседний сайдбар занял больше места. Долгое время фронтенд-разработчикам приходилось подключать тяжелые скрипты с ResizeObserver или getBoundingClientRect(), создавая паразитные перерасчеты макета (layout thrashing) в основном потоке браузера.

Инженер-дизайнер Ахмад Шадид (Ahmad Shadeed) показал, что современная веб-платформа способна решить эту задачу исключительно средствами чистого CSS, объединив спецификации CSS Anchor Positioning, Scroll-Driven Animations и Container Style Queries.

Механика детекции коллизий: измерение зазора и искусственное переполнение

В основе метода лежит элегантный физический трюк. Чтобы понять, наезжает ли один элемент на другой, браузеру необходимо непрерывно вычислять дистанцию между ними. В этом помогает спецификация CSS Anchor Positioning:

  1. Декоративному блоку присваивается якорное имя anchor-name: --decor, а соседнему блоку с текстом — anchor-name: --content;
  2. Между ними создается невидимый служебный элемент .measure, чьи границы через функцию anchor() жестко привязываются к краям соседей: его левая граница крепится к правому краю декора, а правая — к левому краю контента.

В результате ширина элемента .measure всегда строго равна физическому расстоянию между декором и текстом.

Как превратить изменение ширины в дискретный сигнал для браузера? С помощью переполнения (overflow). Внутрь элемента измерения помещается псевдоэлемент ::after фиксированной ширины — например, 24px (минимально допустимый безопасный зазор). Самому блоку .measure выставляется свойство overflow-x: auto. Пока зазор между декором и текстом больше 24px, переполнения нет. Но как только расстояние сокращается до 23px и меньше, в блоке .measure возникает горизонтальный скролл.

Подъем области видимости через timeline-scope и триггер анимации

По умолчанию шкала прокрутки scroll-timeline видна только дочерним элементам самого скроллера. Чтобы декоративный элемент снаружи мог среагировать на возникновение переполнения, используется директива timeline-scope.

Объявление timeline-scope: --collision-timeline на общем родительском контейнере (например, на body) «поднимает» (hoists) именованную шкалу времени наверх, делая ее доступной для независимых веток дерева DOM. Это логическое продолжение инженерных поисков, в которых исследовалась детекция пересечения элементов на чистом CSS.

Связка animation-timeline: --collision-timeline запускает мгновенную микро-анимацию длительностью в 1 миллисекунду. Эта анимация не сдвигает пиксели — она переключает пользовательскую CSS-переменную --is-colliding: 1. Далее вступает в силу спецификация Container Style Queries: декоративный элемент слушает изменение переменной через @container style(--is-colliding: 1) и плавно исчезает через opacity: 0; pointer-events: none;.

Практический сценарий реализации и безопасный фоллбек

Посмотрим, как выглядит полная декларативная связка в CSS. В первом листинге представлена разметка и стили для измерения зазора и скрытия декора:

/* 1. Поднимаем шкалу времени на общий предок */
body {
  --is-colliding: 0;
  timeline-scope: --collision-timeline;
  animation: toggle-collision 1ms linear;
  animation-timeline: --collision-timeline;
}

/* 2. Назначаем имена якорей элементам макета */
.floating-decor {
  anchor-name: --decor;
  transition: opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}

.text-section {
  anchor-name: --content;
}

/* 3. Служебный блок измерения зазора между правым краем декора и левым краем текста */
.measure-gap {
  position: absolute;
  top: anchor(--decor top);
  bottom: anchor(--decor bottom);
  left: anchor(--decor right);
  right: anchor(--content left);
  overflow-x: auto;
  pointer-events: none;
  visibility: hidden;
  scroll-timeline: --collision-timeline x;
}

/* 4. Порог безопасного расстояния (24px) */
.measure-gap::after {
  content: "";
  display: block;
  width: 24px;
  height: 100%;
}

/* 5. Мгновенное переключение флага коллизии при возникновении скролла */
@keyframes toggle-collision {
  from, to {
    --is-colliding: 1;
  }
}

/* 6. Реактивное скрытие декора через Container Style Queries */
.floating-decor {
  @container style(--is-colliding: 1) {
    opacity: 0;
    pointer-events: none;
  }
}

Поскольку связка использует передовые спецификации, критически важно предусмотреть безопасный фоллбек для браузеров без полной поддержки Anchor Positioning:

/* Базовое безопасное поведение: скрытие декора по брейкпоинту */
@media (max-width: 992px) {
  .floating-decor {
    display: none;
  }
}

/* Прогрессивное улучшение: возвращаем декор, если браузер умеет считать коллизии */
@supports (timeline-scope: --test) and (anchor-name: --test) {
  @media (max-width: 992px) {
    .floating-decor {
      display: block;
    }
  }
}

Готовность к продакшену

Сегодня связка полноценно поддерживается в браузерах на базе Chromium версии 125+ (Chrome, Edge, Brave), где реализованы обе спецификации. В Safari (WebKit) и Firefox (Gecko) поддержка Anchor Positioning и timeline-scope активно дорабатывается в предварительных сборках.

Для продуктовых дизайн-систем этот подход задает новый стандарт: компоненты становятся по-настоящему автономными. Вместо жестких глобальных брейкпоинтов и хрупких JavaScript-обработчиков браузер самостоятельно рассчитывает физическое пространство страницы прямо в конвейере рендеринга.