Постепенное изменение числа до целевого значения

В этой статье разберём частый кейс при вёрстке - постепенное/плавное увеличение числа от некоторого значения до целевого

Для этого будем использовать плагин countUp и Intersection Observer API, чтобы запускать увеличение числа только в том случае, когда элемент появится в видимой области браузера

Про Intersection Observer API подробнее можно почитать в предыдущей статье - Вариант бесконечной прокрутки постов

В конце статьи ссылка на итоговый результат


Простой пример работы плагина countUp

Для начала скачаем плагин - так как мы будем использовать плагин без сборщиков, то берём файл countUp.min.js по ссылке https://www.unpkg.com/browse/countup@1.8.2/dist/

Подключаем скачанный файл countUp.min.js перед основным javascript-файлом main.js

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <title>Numbers</title>
    <link rel="stylesheet" href="css/reset.min.css"/>
    <link rel="preconnect" href="https://fonts.googleapis.com"/>
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="crossorigin"/>
    <link href="https://fonts.googleapis.com/css2?family=Rubik&amp;display=swap" rel="stylesheet"/>
    <link rel="stylesheet" href="css/main.css"/>
  </head>
  <body>
    <section class="numbers">
      <div class="container">
        <h2>Numbers</h2>
        <div class="numbers__grid">
          <div class="numbers__item">

            <span id="num1">0</span> <!-- у данного элемента будем постепенно увеличивать значение -->

          </div>
        </div>
      </div>
    </section>
    <script src="js/countUp.min.js"></script>
    <script src="js/main.js"></script>
  </body>
</html>


Напишем немного стилей

body {
  font-family: 'Rubik', sans-serif;
  color: #333;
}
.container {
  max-width: 720px;
  margin: 0 auto;
  padding: 0 16px;
}
h2 {
  text-align: center;
  margin-bottom: 32px;
  font-size: 48px;
}
.numbers {
  padding: 64px 0;
}
.numbers__grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 32px;
  grid-gap: 32px;
}
.numbers__item {
  text-align: center;
  font-size: 48px;
  padding: 48px;
  border-radius: 32px;
  background: #eee;
}


Для того чтобы активировать плагин для элемента <span id="num1">0</span>, зададим необходимые параметры

window.addEventListener('DOMContentLoaded', () => { // Структура страницы загружена и готова к взаимодействию

  const count = new CountUp( // задаем необходимые параметры
    'num1', // 1. задаём идентификатор элемента с числом
    0, // 2. задаём начальное число
    200, // 3. задаём конечное число
    0, // 4. задаём количество цифр после запятой
    4 // 5. задаём продолжительность анимации в секундах
  );
  count.start() // запускаем настроенную анимацию

})

На данный момент при загрузке страницы значение элемента <span id="num1">0</span> будет увеличиваться от 0 до 200, число будет иметь 0 цифр после запятой, и увеличение значение будет происходить на протяжении 4 секунд

Это минимальное использование плагина, но мы хотим сделать несколько карточек с числами, которые будут иметь разные значения, разную продолжительность, разные префиксы и суффиксы для каждого значения, а также запускать увеличение числа только тогда, когда элемент появляется в видимой области браузера, поэтому разберём более сложный пример



Пишем HTML структуру более сложного примера

Подключим сброс стилей - в этот раз будем использовать modern-css-reset. https://piccalil.li/blog/a-modern-css-reset/ - здесь подробнее про него

Добавим контент для наглядности, чтобы карточки с числами были видны не сразу

Также для элементов <span id="...">0</span> указываем data-атрибуты, которые в дальнейшем будем использовать в javascript-коде

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <title>Numbers</title>
    <link rel="stylesheet" href="css/reset.min.css"/> <!-- Подключаем сброс стилей -->
    <link rel="preconnect" href="https://fonts.googleapis.com"/>
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="crossorigin"/>
    <link href="https://fonts.googleapis.com/css2?family=Rubik&amp;display=swap" rel="stylesheet"/> <!-- Подключаем шрифт -->
    <link rel="stylesheet" href="css/main.css"/> <!-- Подключаем основной файл стилей -->
  </head>
  <body>
    <!-- Контент для наглядности -->
    <section class="content">
      <div class="container">
        <h2>Content One</h2>
        <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ab, aliquam aliquid assumenda beatae commodi eaque id illo illum iste, magni maxime nihil optio perferendis praesentium, reprehenderit sapiente tempore vel voluptate. Ad asperiores, commodi culpa cupiditate dignissimos dolor dolorum ea facere incidunt laborum minima nemo, nostrum odit placeat quasi recusandae saepe? Alias dicta dolorum fugit iste quam recusandae tenetur! Aspernatur dignissimos doloremque ipsum magni molestias quasi quisquam quos sed sit tempora.</p>
        <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ab, aliquam aliquid assumenda beatae commodi eaque id illo illum iste, magni maxime nihil optio perferendis praesentium, reprehenderit sapiente tempore vel voluptate. Ad asperiores, commodi culpa cupiditate dignissimos dolor dolorum ea facere incidunt laborum minima nemo, nostrum odit placeat quasi recusandae saepe? Alias dicta dolorum fugit iste quam recusandae tenetur! Aspernatur dignissimos doloremque ipsum magni molestias quasi quisquam quos sed sit tempora.</p>
        <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ab, aliquam aliquid assumenda beatae commodi eaque id illo illum iste, magni maxime nihil optio perferendis praesentium, reprehenderit sapiente tempore vel voluptate. Ad asperiores, commodi culpa cupiditate dignissimos dolor dolorum ea facere incidunt laborum minima nemo, nostrum odit placeat quasi recusandae saepe? Alias dicta dolorum fugit iste quam recusandae tenetur! Aspernatur dignissimos doloremque ipsum magni molestias quasi quisquam quos sed sit tempora.</p>
        <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ab, aliquam aliquid assumenda beatae commodi eaque id illo illum iste, magni maxime nihil optio perferendis praesentium, reprehenderit sapiente tempore vel voluptate. Ad asperiores, commodi culpa cupiditate dignissimos dolor dolorum ea facere incidunt laborum minima nemo, nostrum odit placeat quasi recusandae saepe? Alias dicta dolorum fugit iste quam recusandae tenetur! Aspernatur dignissimos doloremque ipsum magni molestias quasi quisquam quos sed sit tempora.</p>
      </div>
    </section>

    <!-- Нас интересует эта часть с числами -->
    <section class="numbers">
      <div class="container">
        <h2>Numbers</h2>
        <div class="numbers__grid">
          <div class="numbers__item">
            <span id="num1" data-num="35001" data-prefix="$" data-duration="10">0</span>
          </div>
          <div class="numbers__item">
            <span id="num2" data-num="27" data-suffix="%">0</span>
          </div>
          <div class="numbers__item">
            <span id="num3" data-num="124" data-suffix="+" data-duration="6">0</span>
          </div>
          <div class="numbers__item">
            <span id="num4" data-num="5799" data-duration="8">0</span>
          </div>
        </div>
      </div>
    </section>

    <!-- Контент для наглядности -->
    <section class="content">
      <div class="container">
        <h2>Content Two</h2>
        <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Dolore non quibusdam rerum saepe voluptatibus? Ab adipisci commodi consequatur dicta dolor eaque eius esse fugit harum iusto laborum laudantium minima modi mollitia natus nobis nostrum nulla numquam odit placeat quibusdam quisquam quos rerum, sapiente sed similique sit tempora ut veniam veritatis?</p>
        <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Dolore non quibusdam rerum saepe voluptatibus? Ab adipisci commodi consequatur dicta dolor eaque eius esse fugit harum iusto laborum laudantium minima modi mollitia natus nobis nostrum nulla numquam odit placeat quibusdam quisquam quos rerum, sapiente sed similique sit tempora ut veniam veritatis?</p>
        <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Dolore non quibusdam rerum saepe voluptatibus? Ab adipisci commodi consequatur dicta dolor eaque eius esse fugit harum iusto laborum laudantium minima modi mollitia natus nobis nostrum nulla numquam odit placeat quibusdam quisquam quos rerum, sapiente sed similique sit tempora ut veniam veritatis?</p>
        <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Dolore non quibusdam rerum saepe voluptatibus? Ab adipisci commodi consequatur dicta dolor eaque eius esse fugit harum iusto laborum laudantium minima modi mollitia natus nobis nostrum nulla numquam odit placeat quibusdam quisquam quos rerum, sapiente sed similique sit tempora ut veniam veritatis?</p>
        <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Dolore non quibusdam rerum saepe voluptatibus? Ab adipisci commodi consequatur dicta dolor eaque eius esse fugit harum iusto laborum laudantium minima modi mollitia natus nobis nostrum nulla numquam odit placeat quibusdam quisquam quos rerum, sapiente sed similique sit tempora ut veniam veritatis?</p>
      </div>
    </section>
    <script src="js/countUp.min.js"></script> <!-- Подключаем плагин countUp -->
    <script src="js/main.js"></script> <!-- Подключаем основной javascript файл -->
  </body>
</html>


Добавим CSS стили для нового примера

Для блока с числами используем CSS Grid

Для карточек с числами опишем модификатор numbers__item_done, который будет менять фон карточки при достижении целевого значения

body {
  font-family: 'Rubik', sans-serif;
  color: #333;
}
.container {
  max-width: 720px;
  margin: 0 auto;
  padding: 0 16px;
}
.content {
  padding: 64px 0;
}
h2 {
  text-align: center;
  margin-bottom: 32px;
  font-size: 48px;
}
p {
  text-align: justify;
  font-size: 18px;
}
p:not(:last-child) {
  margin-bottom: 32px;
}
.numbers__grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 32px;
  grid-gap: 32px;
}
.numbers__item {
  text-align: center;
  font-size: 48px;
  padding: 48px;
  border-radius: 32px;
  background: #eee;
  transition: 0.8s;
}
.numbers__item_done {
  background: #56e39f;
}
@media (max-width: 767.98px) {
  .numbers__grid {
    grid-template-columns: 1fr;
  }
}


Пишем JavaScript код

Повторюсь, мы будем использовать Intersection Observer API, чтобы запускать увеличение числа только в том случае, когда элемент появится в видимой области браузера (подробнее можно почитать в предыдущей статье - Вариант бесконечной прокрутки постов)

Если ниже по тексту будет что-то непонятно, попробуйте сначала посмотреть код с комментариями ниже, а потом вернитесь сюда

Кроме необходимых настроек, плагин позволяет нам передавать дополнительные параметры в виде объекта - в данном примере мы добавим разделитель групп разрядов (separator), префикс (prefix) и suffix (suffix)

Полный список параметров можно посмотреть в документации плагина - https://github.com/inorganik/countUp.js

Чтобы параметры для каждого элемента были индивидуальными, будем использовать data-атрибуты - про использование data-атрибутов можно почитать здесь - https://developer.mozilla.org/ru/docs/Learn/HTML/Howto/Use_data_attributes

Так как мы используем Intersection Observer API то, чтобы получить, например, значение атрибута data-suffix="%" из наблюдаемого элемента, будем использовать следующий синтаксис - entry.target.dataset.suffix

Но если данный data-атрибут у наблюдаемого элемента отсутствует, то будем назначать значение по-умолчанию - suffix: entry.target.dataset.suffix || '' - если есть атрибут data-suffix, то подставляем его значение, если нет, то назначаем пустую строку

Например, у элемента <span id="num2" data-num="27" data-suffix="%">0</span> указан атрибут data-suffix="%", значит назначаем суффикс %, а data-prefix отсутствует, поэтому префикс будет пустой строкой, то есть его не будет

Также плагин предоставляет возможность запускать дополнительную функцию при достижении целевого значения числа, в данном примере мы будем добавлять активный класс карточке. Как только постепенное увеличение числа закончит выполняться, фон карточки будет изменён

Код с комментариями

window.addEventListener('DOMContentLoaded', () => { // Структура страницы загружена и готова к взаимодействию

  const countNums = () => { // объявляем функцию, чтобы всё, что относится к анимированию чисел было в одном месте
    const numbersObserver = new IntersectionObserver((entries, observer) => { // создаём наблюдатель за элементами, в которых будем увеличивать значение числа
      entries.forEach(entry => { // для каждого наблюдаемого элемента
        if (entry.isIntersecting) { // проверяем, находится ли он в видимой области браузера
          const count = new CountUp( // настраиваем новую анимацию для числа
            entry.target.id, // 1. задаём идентификатор элемента с числом
            0, // 2. задаём начальное число
            entry.target.dataset.num, // 3. задаём конечное число (берем из data-атрибута)
            0, // 4. задаём количество цифр после запятой
            entry.target.dataset.duration || 4, // 5. задаём продолжительность анимации в секундах (если у элемента есть атрибут data-duration, то берём из него значение, иначе назначаем 4 секунды по-умолчанию)
            { // указываем дополнительные параметры
              separator: ' ', // задаём разделитель групп разрядов (например для миллиона - 1 000 000)
              prefix: entry.target.dataset.prefix || '', // задаём префикс - любые символы перед числом (берем значение из data-prefix, если не указано - то задаем пустую строку по умолчанию)
              suffix: entry.target.dataset.suffix || '' // задаём суффикс - любые символы после числа (берем значение из data-suffix, если не указано - то задаем пустую строку по умолчанию)
            }
          );
          count.start(() => { // запускаем настроенную анимацию и по окончании анимации...
            entry.target.parentElement.classList.add('numbers__item_done') // ...добавляем активный класс родительскому элементу
          })
          observer.unobserve(entry.target); // отключаем наблюдение за элементом
        }
      })
    });
    document.querySelectorAll('.numbers__item span').forEach(num => { // ищем элементы за которыми будем наблюдать, и для каждого...
      numbersObserver.observe(num) // ...запускаем наблюдение
    })
  }
  countNums() // запускаем объявленную функцию

})


Получаем следующий результат

0
0
0
0

Можно скачать архив с результатом



Итоги

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

Буду рад, если статья оказалась полезной

Спасибо за ваше внимание и уделённое время!