Использование CSS анимации, основанной на прокрутке, для индикации прогресса прокрутки на основе секций

Источник: «Using CSS Scroll-Driven Animations for Section-Based Scroll Progress Indicators»
Индикатор прогресса прокрутки — довольно простая вещь, которую можно создать с помощью анимации, основанной на прокрутке в scroll()-стиле. Но мы создадим индикаторы для каждого раздела страницы, используя view()-стиль.

Анимации на основе прокрутки позволяют управлять анимацией на основе прогресса прокрутки любого отдельного элемента (часто всего документа) или прогресса видимости определённого элемента в документе. Это view() и scroll() анимации, соответственно. Обе полезны! Бывает полезно применить анимацию непосредственно к самому элементу, например, чтобы <section> сдвигался на место, когда он попадает в область просмотра. Такие вещи круты и интересны, но задумывались ли вы о том, чтобы расширить эффекты этих анимаций за пределы вызывающих их элементов?

В CSS анимация на основе прокрутки осуществляется с помощью нескольких функций анимации: scroll() и view(). Подробнее о них вы можете узнать здесь.

В статье используется анимация view() в сочетании с пользовательским свойством CSS, объявленным с помощью @property, для создания индикатора прогресса текущего просмотра и прогресса по разделам для каждого раздела страницы. Подобные вещи могут быть полезны, например, для длинной страницы документации, чтобы пользователь мог видеть, где он находится, и как далеко он продвинулся в текущем разделе. Что-то вроде индикатора прогресса чтения, но более умный, поскольку он учитывает отдельные разделы страницы.

Вот демонстрация:

See the Pen

Временная шкала view() будет отслеживать положение каждой секции во время прокрутки, а @property поможет передать анимируемый результат прокрутки каждой секции в её элемент-индикатор.

HTML основа

Для начала давайте разберёмся с HTML элементами. Нам понадобятся:

  1. <section> страницы
  2. Полосы индикации прогресса прокрутки

Вот оба:

<section id="one">
Section number one
<span>First</span>
</section>

<section id="two">
Second section
<span>Second</span>
</section>

<section id="three">
Third section
<span>Third</span>
</section>

<section id="four">
Final section
<span>Fourth</span>
</section>

<span> — это элементы индикатора, которые вскоре будут перемещены в правый верхний угол области просмотра, где останутся неподвижными, пока пользователь прокручивает страницу.

CSS для секций и индикаторов

section {
width: 400px;
aspect-ratio: 1 / 2;
/* ... */
}
span {
position: fixed;
height: 1lh;
line-height: 40px;
width: 100px;
right: 60px;
--t: 60px; /* переменная top */
--h: calc(1lh + 10px); /* для промежутка между span */
section:nth-of-type(1) &{
top: var(--t);
}
section:nth-of-type(2) &{
top: calc(var(--t) + var(--h));
}
section:nth-of-type(3) &{
top: calc(var(--t) + 2 * var(--h));
}
section:nth-of-type(4) &{
top: calc(var(--t) + 3 * var(--h));
}
&::before { /* синяя полоса */
display: block;
position: absolute;
content: '';
width: 4px;
height: inherit;
background: rgb(55,126,245);
/* ... */
}
}

span присваивается position: fixed и значение right, чтобы зафиксировать их сбоку экрана. Значение top каждого span измеряется (с помощью calc()) на основе их высоты и промежутка между ними. При необходимости можно рассмотреть альтернативные логические свойства для этих значений, если есть основания полагать, что страница, над которой вы работаете, может быть переведена.

Псевдоэлемент в span используется в качестве фактического индикатора прогресса — синяя линия, которая увеличивается/уменьшается на каждом поле индикатора.

See the Pen

Переходим к самой интересной части — анимации!

Анимация на основе прокрутки

Мы будем показывать прогресс прокрутки каждого раздела, анимируя height каждой полосы индикатора по мере того, как пользователь прокручивает его. Ну, возможно, не height, а само CSS свойство, но визуальная высота. На самом деле будем использовать scaleY() для полос, так как это считается более эффективным способом анимации. Функция scaleY() принимает значение типа number, поэтому необходимо объявить это свойство с помощью @property. Итак, давайте зарегистрируем пользовательское свойство, принимающее числовое значение, и настроим анимацию @keyframes, которая, собственно, и будет выполнять работу:

@property --n {
syntax: "<number>";
inherits: true;
initial-value: 0;
}
@keyframes slide {
from { --n: 0; }
to { --n: 1; }
}

Важно, чтобы при объявлении пользовательского свойства атрибут inherits был равен true. Это гарантирует, что значение будет доступно для span'ов, даже если свойства animation- добавлены к секциям. span наследует значение как таковое.

section {
animation-timeline: view(block 98% 2%);
animation-name: slide;
animation-fill-mode: both;
/* Оставшийся код из первого css сниппета находится здесь */
}
span {
/* Оставшийся код из первого css сниппета находится здесь */
&::before { /* синяя полоса */
/* Оставшийся код из первого css сниппета находится здесь */
transform: scaleY(var(--n)); /* здесь происходит анимация */
transform-origin: top;
}
}

Я предпочитаю, чтобы прогресс прокрутки измерялся относительно (концептуальной) горизонтальной линии, расположенной близко к нижней части экрана. Каждый раз, когда секция проходит через эту линию, её временная шкала анимации перемещается вперёд или назад в зависимости от направления прокрутки (вверх или вниз). Область, которую я выбрал для этого, находится на расстоянии 2% от нижней части экрана. Вот разбивка значений функции view().

  1. block — область определяется по оси блока. Ось блока — это вертикальная ось для направления текста слева направо.
  2. 98% — определяемая область начинается на 98% от верха экрана (или на 98% от начала оси блока)
  3. 2% — область заканчивается на расстоянии 2% от нижней части экрана (или 2% от конца оси блока)

Поскольку я хотел, чтобы заданная область представляла собой линию, то не оставил никакого пространства между началом и концом области. Однако при желании можно расширить область. Например, 70% 20% даёт область длиной 10% на экране, по которой будет измеряться прогресс прокрутки. Или вы можете даже переместить область в верхнюю часть экрана. 0 100% назначит самый верх области просмотра в качестве маркера для отслеживания прогресса прокрутки.

Затем шкала прогресса перемещается по ключевым кадрам анимации, которую назвали slide, обновляя пользовательскую переменную --n соответствующим значением от 0 до 1. Здесь используются 0 и 1, потому что 0 означает масштабировать эту полосу до 0% высоты, а 1 означает масштабировать эту полосу до 100% высоты.

Параметр transform-origin: top задаёт масштабирование сверху элемента полосы, то есть полоса будет выглядеть так, будто она растёт сверху вниз, что имитирует прокрутку.

Вот окончательный результат:

See the Pen

Вариация

Если предпочитаете, чтобы вначале не было полосы для первой секции, по крайней мере, до тех пор, пока вы прокрутите страницу немного вниз, ограничьте диапазон анимации первой section желаемым значением. Это может показаться вам (и/или пользователям) более удобным, потому что если они вообще не прокручивали страницу, может быть странно визуально показывать раздел как частично завершённый. Вот эта вариация:

section:nth-of-type(1) {
animation-range: contain 70%;
}

Анимация не начнётся, пока отметка 70% первой секции не пересечёт область на экране, заданную функцией view(). Эффект наблюдается, когда область просмотра короче первой секции. Настройте его так, как нужно.

See the Pen

Заключение

Вот и всё! Надеюсь, статья дала представление о том, как можно каскадировать значение позиции прокрутки и прогресс анимации за пределы непосредственно назначенного элемента, что позволяет создавать более динамичные дизайны на основе прокрутки с помощью одного лишь CSS. Зная, что вы можете передавать значение пользовательского свойства другим элементам, можно подумать о том, как запросы стилей могут помочь в реализации этой идеи 🤔.

Дополнительные материалы

Предыдущая Статья

Сокращаем размер конфигов Symfony до минимума

Следующая Статья

Как использовать Corepack