Индикатор прокрутки на JavaScript

С нуля напишем индикатор прокрутки страницы на чистом JavaScript.

1

Посмотреть данный пример можно на Codepen

Создадим HTML структуру

Сделаем блок <div class="content"></div> с большим количеством текста для тестирования индикатора прокрутки

Элемент индикатора прокрутки в разметку не добавляем, так как будем создавать и добавлять его динамически через JavaScript

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"/>
    <title></title>
    <link rel="stylesheet" href="css/bootstrap-reboot.min.css"/>
    <link rel="stylesheet" href="css/main.css"/>
  </head>
  <body>
    <div class="header">
      <div class="header__logo">Progress</div>
    </div>
    <div class="content">
      <p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Rem sapiente amet fugiat possimus dolorum aut quos, perspiciatis consequuntur pariatur! Qui non a magni. Magnam officiis beatae vitae animi sint ullam enim non quidem ipsam commodi deleniti eius, ipsum voluptates? Animi, cupiditate sunt nam alias asperiores quam, accusamus architecto illo nostrum vel, itaque debitis distinctio non nobis omnis incidunt dolore dicta deserunt repudiandae eveniet voluptatem nemo dolor soluta. Explicabo nisi quisquam reprehenderit et necessitatibus voluptatum! Ipsa, laboriosam. Voluptatibus quis esse voluptates id hic at odit? Eos sunt iure architecto quas quae eveniet vitae unde, dolorem quaerat sequi assumenda explicabo eaque distinctio dolores soluta quia deserunt perferendis, doloremque iste temporibus aliquid nihil et quasi amet. Culpa quas enim maxime, nihil officiis blanditiis et, in illo, aliquid natus labore! Culpa dicta praesentium voluptatum, veritatis dolor corporis, quis repudiandae voluptate maxime delectus inventore, officia ipsam sunt aspernatur pariatur natus aliquid et in. Dicta ipsa nobis suscipit est velit minus eligendi natus obcaecati consequuntur mollitia! Nemo quas necessitatibus unde facere sapiente repudiandae sit iste rem, reprehenderit temporibus natus nisi accusantium cum recusandae laborum enim a, id mollitia omnis odio amet eligendi, labore distinctio? Maxime nemo optio quis dicta maiores possimus officia at officiis molestiae voluptatem perferendis labore neque distinctio, similique commodi provident voluptatibus laudantium facilis porro quo non sed alias eos! Vero repudiandae quos cum ad dolor error sint sit eos ullam rem quam, deleniti soluta? Possimus voluptatum quas vero ducimus! Debitis, ipsa? Tenetur, excepturi saepe molestiae vel, exercitationem, sed possimus nihil porro asperiores maiores iure sapiente obcaecati dolor quod dignissimos totam laboriosam labore. Fugiat alias odit recusandae maiores, modi non veritatis perspiciatis accusantium quisquam consectetur, velit, sunt illum? A ut sint, temporibus harum omnis autem impedit ipsam laudantium ullam. Possimus, rerum quod fuga expedita quos dignissimos nostrum doloremque provident doloribus iste laudantium earum nihil cumque temporibus dolores odit culpa pariatur veritatis. Debitis voluptas aliquid vero? Maiores illum laboriosam, sed qui dicta alias. Dolorum unde incidunt explicabo impedit dignissimos iure, voluptates, qui fuga quis optio neque et cum voluptatem rerum architecto eos similique accusantium velit nobis, harum minus ipsum repellendus? Iusto praesentium provident sed consequatur non laborum voluptatem rerum modi earum neque fuga architecto possimus est vitae, vero eaque ut minima officia suscipit eligendi aliquam quae obcaecati quod exercitationem. Exercitationem tenetur, est eveniet consectetur nobis temporibus, corporis voluptates omnis laboriosam ipsam impedit, aut porro consequuntur autem quam. Eaque reiciendis velit, explicabo repellat harum suscipit perferendis aspernatur iusto minus rerum fugiat omnis blanditiis accusantium enim corrupti voluptates repellendus officiis cum quos pariatur veniam. Quasi mollitia voluptates repellat distinctio veritatis excepturi facere corporis iure incidunt provident neque dolor quibusdam, iusto eum. Quidem consequatur esse enim numquam blanditiis illo veritatis deserunt fuga a iusto, debitis, explicabo totam architecto aliquam quam aperiam incidunt perferendis, nemo magni dolores aut facere. Atque, fugit cupiditate illo, aliquid doloremque officiis perferendis non quam exercitationem accusamus, ullam consequuntur incidunt amet nam quae totam. Repellendus, enim eius. Excepturi officia sint reiciendis sequi, est eum atque et quaerat quam incidunt numquam error delectus nam, nesciunt nemo!</p>
      <p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Rem sapiente amet fugiat possimus dolorum aut quos, perspiciatis consequuntur pariatur! Qui non a magni. Magnam officiis beatae vitae animi sint ullam enim non quidem ipsam commodi deleniti eius, ipsum voluptates? Animi, cupiditate sunt nam alias asperiores quam, accusamus architecto illo nostrum vel, itaque debitis distinctio non nobis omnis incidunt dolore dicta deserunt repudiandae eveniet voluptatem nemo dolor soluta. Explicabo nisi quisquam reprehenderit et necessitatibus voluptatum! Ipsa, laboriosam. Voluptatibus quis esse voluptates id hic at odit? Eos sunt iure architecto quas quae eveniet vitae unde, dolorem quaerat sequi assumenda explicabo eaque distinctio dolores soluta quia deserunt perferendis, doloremque iste temporibus aliquid nihil et quasi amet. Culpa quas enim maxime, nihil officiis blanditiis et, in illo, aliquid natus labore! Culpa dicta praesentium voluptatum, veritatis dolor corporis, quis repudiandae voluptate maxime delectus inventore, officia ipsam sunt aspernatur pariatur natus aliquid et in. Dicta ipsa nobis suscipit est velit minus eligendi natus obcaecati consequuntur mollitia! Nemo quas necessitatibus unde facere sapiente repudiandae sit iste rem, reprehenderit temporibus natus nisi accusantium cum recusandae laborum enim a, id mollitia omnis odio amet eligendi, labore distinctio? Maxime nemo optio quis dicta maiores possimus officia at officiis molestiae voluptatem perferendis labore neque distinctio, similique commodi provident voluptatibus laudantium facilis porro quo non sed alias eos! Vero repudiandae quos cum ad dolor error sint sit eos ullam rem quam, deleniti soluta? Possimus voluptatum quas vero ducimus! Debitis, ipsa? Tenetur, excepturi saepe molestiae vel, exercitationem, sed possimus nihil porro asperiores maiores iure sapiente obcaecati dolor quod dignissimos totam laboriosam labore. Fugiat alias odit recusandae maiores, modi non veritatis perspiciatis accusantium quisquam consectetur, velit, sunt illum? A ut sint, temporibus harum omnis autem impedit ipsam laudantium ullam. Possimus, rerum quod fuga expedita quos dignissimos nostrum doloremque provident doloribus iste laudantium earum nihil cumque temporibus dolores odit culpa pariatur veritatis. Debitis voluptas aliquid vero? Maiores illum laboriosam, sed qui dicta alias. Dolorum unde incidunt explicabo impedit dignissimos iure, voluptates, qui fuga quis optio neque et cum voluptatem rerum architecto eos similique accusantium velit nobis, harum minus ipsum repellendus? Iusto praesentium provident sed consequatur non laborum voluptatem rerum modi earum neque fuga architecto possimus est vitae, vero eaque ut minima officia suscipit eligendi aliquam quae obcaecati quod exercitationem. Exercitationem tenetur, est eveniet consectetur nobis temporibus, corporis voluptates omnis laboriosam ipsam impedit, aut porro consequuntur autem quam. Eaque reiciendis velit, explicabo repellat harum suscipit perferendis aspernatur iusto minus rerum fugiat omnis blanditiis accusantium enim corrupti voluptates repellendus officiis cum quos pariatur veniam. Quasi mollitia voluptates repellat distinctio veritatis excepturi facere corporis iure incidunt provident neque dolor quibusdam, iusto eum. Quidem consequatur esse enim numquam blanditiis illo veritatis deserunt fuga a iusto, debitis, explicabo totam architecto aliquam quam aperiam incidunt perferendis, nemo magni dolores aut facere. Atque, fugit cupiditate illo, aliquid doloremque officiis perferendis non quam exercitationem accusamus, ullam consequuntur incidunt amet nam quae totam. Repellendus, enim eius. Excepturi officia sint reiciendis sequi, est eum atque et quaerat quam incidunt numquam error delectus nam, nesciunt nemo!</p>
    </div>
    <script src="js/main.js"></script>
  </body>
</html>


Пишем CSS стили

Стилей для индикатора прокрутки также не пишем, они будут добавляться через JavaScript

.header {
  display: flex;
  justify-content: center;
  align-items: center;
  padding: 15px 30px;
  background: #fff;
  position: -webkit-sticky;
  position: sticky;
  top: 0;
  transition: 1s;
  color: #7b4397;
  box-shadow: 0 3px 15px rgba(123,67,151,0.3);
}
.header__logo {
  font-size: 24px;
  font-weight: bold;
}
.header_hidden {
  transform: translateY(-100%);
}
.content {
  max-width: 520px;
  margin: 0 auto;
  padding: 60px 30px;
}



Пишем JavaScript код

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

window.onload = () => { // Страница загружена полностью вместе с изображениями, стилями и тд

  const progress = () => { // объявляем основную функцию индикатора прокрутки

    const line = document.createElement('div') // создаем элемент <div>
    line.className = 'progress' // назначаем ему класс progress
    line.style.cssText = `
      height: 6px;
      background: linear-gradient(to right, #7b4397, #dc2430);
      position: fixed;
      top: 0;
      left: 0;
      transition: 1s;
      z-index: 99999;
    ` // добавляем инлайновые стили

    document.body.prepend(line) // вставляем созданный элемент <div> в начало <body>

    const progressWidth = () => { // объявляем функцию расчета ширины элемента <div>
      return line.style.width = Math.floor(window.pageYOffset / (document.documentElement.scrollHeight - window.innerHeight) * 100) + '%'
    }

    progressWidth() // вызываем функцию progressWidth, когда страница загружена, для корректного отображения ширины индикатора прокрутки

    document.addEventListener('scroll', throttle(progressWidth, 64)) // вызываем функцию при прокрутке
    window.addEventListener('resize', throttle(progressWidth, 64)) // вызываем функцию при изменения размеров окна

  }

  progress() // вызываем основную функцию индикатора прокрутки

}

// функция throttle будет ограничивать частоту вызовов функции progressWidth
const throttle = (func, ms) => { // объявляем функцию throttle и передаем параметры: func - функция, частоту вызовов которой будем ограничивать, ms - время, которое должно пройти между предыдущим и следующим вызовом функции func
  let locked = false // создаем переменную, которая будет блокировать вызов функции
  return () => { // создаем и возвращаем анонимную функцию, которая будет иметь доступ к переменной locked
    if (locked) return // если заблокировано, то прекращаем выполнение функции
    locked = true // следующий вызов анонимной функции блокируется, пока не истечет переданное время в ms
    setTimeout(() => { // когда истекает переданное время в ms, внутренний код выполняется
      func() // выполняем переданную функцию func
      locked = false // снимаем блокировку
    }, ms) // подставляем переданное время в ms
  }
}


Рассмотрим код пошагово

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

window.onload = () => { // Страница загружена полностью вместе с изображениями, стилями и тд

}

Создадим основную функцию, чтобы вся логика индикатора прокрутки была в одном месте

window.onload = () => {
  const progress = () => { // объявляем основную функцию индикатора прокрутки

  }
}

Сразу же вызовем эту функцию для просмотра результата в браузере

window.onload = () => {

  const progress = () => {

  }

  progress() // вызываем основную функцию индикатора прокрутки

}

Создадим элемент <div></div>

window.onload = () => {

  const progress = () => {

    const line = document.createElement('div') // создаем элемент <div></div>

  }

  progress()

}

Назначим ему класс progress - <div class="progress"></div>

window.onload = () => {

  const progress = () => {

    const line = document.createElement('div')
    line.className = 'progress' // назначаем ему класс progress

  }

  progress()

}

Так как мы создали новый элемент, то можем ему назначить инлайновые стили через line.style.cssText.

cssText перезаписывает все существующие стили, поэтому его актуально использовать для новых элементов

Стили в cssText запишем используя обратные кавычки для лучшей читаемости

window.onload = () => {

  const progress = () => {

    const line = document.createElement('div')
    line.className = 'progress'
    line.style.cssText = `
      height: 6px;
      background: linear-gradient(to right, #7b4397, #dc2430);
      position: fixed;
      top: 0;
      left: 0;
      transition: 1s;
      z-index: 99999;
    ` // добавляем инлайновые стили

  }

  progress()

}

В результате получим такой элемент - <div class="progress" style="height: 6px; background: rgba(0, 0, 0, 0) linear-gradient(to right, rgb(123, 67, 151), rgb(220, 36, 48)) repeat scroll 0% 0%; position: fixed; top: 0px; left: 0px; transition: all 1s ease 0s; z-index: 99999;"></div>

Вставим созданный элемент в начало <body>

window.onload = () => {

  const progress = () => {

    const line = document.createElement('div')
    line.className = 'progress'
    line.style.cssText = `
      height: 6px;
      background: linear-gradient(to right, #7b4397, #dc2430);
      position: fixed;
      top: 0;
      left: 0;
      transition: 1s;
      z-index: 99999;
    `
    document.body.prepend(line) // вставляем созданный элемент <div> в начало <body>

  }

  progress()

}

Напишем функцию progressWidth, которая будет рассчитывать и возвращать ширину индикатора прокрутки в процентах в зависимости от прокрутки страницы

Логика расчета - узнаём на сколько страница прокручена и делим это значение на разницу полной высоты страницы и высоты окна браузера, и полученный результат умножаем на 100, чтобы получить значение в процентах

Math.floor() - округляет до ближайшего меньшего целого числа

window.onload = () => {

  const progress = () => {

    const line = document.createElement('div')
    line.className = 'progress'
    line.style.cssText = `
      height: 6px;
      background: linear-gradient(to right, #7b4397, #dc2430);
      position: fixed;
      top: 0;
      left: 0;
      transition: 1s;
      z-index: 99999;
    `
    document.body.prepend(line)

    const progressWidth = () => { // объявляем функцию расчета ширины элемента <div>
      return line.style.width = Math.floor(window.pageYOffset / (document.documentElement.scrollHeight - window.innerHeight) * 100) + '%'
    }

  }

  progress()

}

Вызовем функцию progressWidth сразу после объявления, чтобы индикатор прокрутки отображался корректно при загрузке страницы

window.onload = () => {

  const progress = () => {

    const line = document.createElement('div')
    line.className = 'progress'
    line.style.cssText = `
      height: 6px;
      background: linear-gradient(to right, #7b4397, #dc2430);
      position: fixed;
      top: 0;
      left: 0;
      transition: 1s;
      z-index: 99999;
    `
    document.body.prepend(line)

    const progressWidth = () => {
      return line.style.width = Math.floor(window.pageYOffset / (document.documentElement.scrollHeight - window.innerHeight) * 100) + '%'
    }

    progressWidth() // вызываем функцию progressWidth сразу после объявления, чтобы индикатор прокрутки отображался корректно при загрузке страницы

  }

  progress()

}

Также будем вызывать функцию progressWidth при прокрутке страницы и изменении размеров окна браузера

Передаем только название функции progressWidth без круглых скобок, так как необходимо чтобы она вызывалась только при событии прокрутки страницы или изменении размеров окна браузера

window.onload = () => {

  const progress = () => {

    const line = document.createElement('div')
    line.className = 'progress'
    line.style.cssText = `
      height: 6px;
      background: linear-gradient(to right, #7b4397, #dc2430);
      position: fixed;
      top: 0;
      left: 0;
      transition: 1s;
      z-index: 99999;
    `
    document.body.prepend(line)

    const progressWidth = () => {
      return line.style.width = Math.floor(window.pageYOffset / (document.documentElement.scrollHeight - window.innerHeight) * 100) + '%'
    }

    progressWidth()

    document.addEventListener('scroll', progressWidth) // вызываем функцию progressWidth при каждом событии прокрутки страницы
    window.addEventListener('resize', progressWidth) // вызываем функцию progressWidth при каждом событии изменения размеров окна браузера

  }

  progress()

}

Индикатор прокрутки готов, но я хотел бы обратить внимание на оптимизацию при использовании событий прокрутки страницы и изменении размеров окна браузера - об этом ниже



Функция-декоратор throttle

События при прокрутке страницы или изменении размеров окна браузера происходят с очень высокой частотой, то есть функция progressWidth будет вызываться и производить расчеты очень много раз за короткий промежуток времени. При одной прокрутке колесика мыши может срабатывать около 20 событий. Это влияет на производительность. Чтобы ограничить количество вызовов, напишем простую реализацию функции-декоратора throttle

Функция-декоратор - оборачивает исходную функцию, для дополнительных возможностей работы с ней

const throttle = (func, ms) => { // объявляем функцию throttle и передаем параметры: func - функция, частоту вызовов которой будем ограничивать, ms - время, которое должно пройти между предыдущим и следующим вызовом функции func
  let locked = false // создаем переменную, которая будет блокировать вызов функции
  return () => { // создаем и возвращаем анонимную функцию, которая будет иметь доступ к переменной locked
    if (locked) return // если заблокировано, то прекращаем выполнение функции
    locked = true // следующий вызов функции блокируется, пока не истечет переданное время в ms
    setTimeout(() => { // когда истекает переданное время в ms, внутренний код выполняется
      func() // выполняем переданную функцию func
      locked = false // снимаем блокировку
    }, ms) // подставляем переданное время в ms
  }
}


Рассмотрим функцию throttle по шагам

Объявляем функцию throttle с параметрами func и ms, которая будет ограничивать частоту вызова функции func временным промежутком указанным в миллисекундах ms

Параметр func - исходная функция, которую будем оборачивать для дополнительных возможностей, в данном случае функция progressWidth для расчета ширины индикатора прокрутки

Параметр ms - временной промежуток в миллисекундах, на время которого будет блокироваться вызов функции func при высокой частоте вызовов

const throttle = (func, ms) => {

}

Объявим переменную locked, которая будет отвечать за блокировку вызова функции func пока не закончится промежуток времени, переданный в парамере ms

const throttle = (func, ms) => {
  let locked = false // создаем переменную, которая будет блокировать вызов функции
}

Создадим анонимную функцию, код которой будет выполнятся в зависимости от значения переменной locked

const throttle = (func, ms) => {
  let locked = false
  return () => {

  }
}

Анонимная функция имеет доступ к переменной locked.

Если значение переменной locked равно true, то прерываем выполнение функции и дальнейший код не будет выполнен

const throttle = (func, ms) => {
  let locked = false
  return () => {
    if (locked) return
  }
}

Изначально значение переменной locked равно false, поэтому анонимная функция продолжает выполняться

Чтобы заблокировать следующие вызовы анонимной функции, установим значение переменной в true

const throttle = (func, ms) => {
  let locked = false
  return () => {
    if (locked) return
    locked = true
  }
}

Используем метод setTimeout для отложенного запуска части кода и в качестве задержки подставляем переданный параметр ms

const throttle = (func, ms) => {
  let locked = false
  return () => {
    if (locked) return
    locked = true
    setTimeout(() => { // когда истекает переданное время в ms, внутренний код выполняется

    }, ms)
  }
}

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

const throttle = (func, ms) => {
  let locked = false
  return () => {
    if (locked) return
    locked = true
    setTimeout(() => {
      func() // выполняем переданную функцию func
      locked = false // снимаем блокировку
    }, ms)
  }
}

Теперь при событиях прокрутки страницы или изменении размеров окна браузера будем передавать функцию progressWidth не напрямую, а через функцию-декоратор throttle

Выглядить это будет следующим образом

document.addEventListener('scroll', throttle(progressWidth, 64)) // вызываем функцию при прокрутке
window.addEventListener('resize', throttle(progressWidth, 64)) // вызываем функцию при изменения размеров окна

Для понимания мы могли создать переменную/константу для результата функции throttle(progressWidth, 64) таким образом

const progressThrottle = throttle(progressWidth, 64)

В константе progressThrottle хранилась бы анонимная функция, которая находится внутри функции throttle, но уже с переданными параметрами - функцией progressWidth, и временем блокировки 64

Тогда выглядело бы это следующим образом

const progressThrottle = throttle(progressWidth, 64)

document.addEventListener('scroll', progressThrottle) // вызываем функцию при прокрутке
window.addEventListener('resize', progressThrottle) // вызываем функцию при изменения размеров окна

Опять же передаем только название функции progressThrottle без круглых скобок, так как необходимо чтобы она вызывалась только при событии прокрутки страницы или изменении размеров окна браузера

Написанную функцию throttle можно использовать и для других функций, нужно всего лишь в параметрах передать необходимую функцию и желаемое время блокировки



Итоги

В результате мы с нуля написали полностью динамически создаваемый, адаптивный и оптимизированный индикатор прокрутки, который можно использовать в любой верстке, настраивать инлайновые стили отталкиваясь от цветовой схемы сайта.

Разобрались с простой реализацией функции-декоратора для оптимизации высокочастотных вызовов при прокрутке страницы и изменении размеров окна, которую также можно использовать в верстке при необходимости оптимизации



Полезные ссылки

Функции

Замыкание

Планирование: setTimeout и setInterval

Размеры и прокрутка окна