W3docs

Прокрутка в JavaScript

События прокрутки в JavaScript позволяют взаимодействовать с прокруткой страницы. Это полезно для ленивой загрузки и других задач.

Понимание событий и техник прокрутки в JavaScript

События прокрутки events в JavaScript позволяют реагировать на положение пользователя на странице. Это основа для таких возможностей, как параллакс-эффекты, запуск анимаций по позиции прокрутки, реализация бесконечной прокрутки, кнопки «наверх», залипающих заголовков и индикаторов прогресса чтения. В этом руководстве рассматривается, как считывать текущую позицию прокрутки, как прокручивать страницу программно, как эффективно обрабатывать событие scroll и когда стоит использовать современный IntersectionObserver.

Считывание текущей позиции прокрутки

Прежде чем реагировать на прокрутку, как правило, нужно знать, насколько страница уже прокручена. Наиболее надёжные свойства доступны через window:

  • window.scrollY — вертикальное смещение прокрутки в пикселях (также window.pageYOffset — устаревший псевдоним).
  • window.scrollX — горизонтальное смещение прокрутки в пикселях (псевдоним window.pageXOffset).

Для отдельного прокручиваемого элемента используйте element.scrollTop и element.scrollLeft. Эти свойства доступны и для записи: присвоение значения переместит элемент на соответствующую позицию.

// How far down the whole page has the user scrolled?
console.log(window.scrollY); // e.g. 0 at the top, 420 partway down

// Total scrollable height of the document
const docHeight = document.documentElement.scrollHeight;
const winHeight = window.innerHeight;

// How close to the bottom are we (0 = top, 1 = bottom)?
const progress = window.scrollY / (docHeight - winHeight);
console.log(progress);

О смещениях отдельных элементов и подробностях работы с getBoundingClientRect читайте в статьях Координаты в JavaScript и Размеры окна и прокрутка.

Программная прокрутка

Вы не только можете слушать прокрутку — вы можете её инициировать. Следующие методы принимают опцию behavior: 'smooth' для анимированного перехода вместо мгновенного скачка:

  • window.scrollTo(x, y) — прокрутить к абсолютной позиции.
  • window.scrollBy(dx, dy) — прокрутить на относительное расстояние от текущей позиции.
  • element.scrollIntoView(options) — прокрутить так, чтобы конкретный элемент стал видимым.
// Jump to the very top, smoothly
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' });

// Nudge down by one viewport height
window.scrollBy({ top: window.innerHeight, behavior: 'smooth' });

// Bring an element into view (e.g. after navigating to an anchor)
document.querySelector('#section-2')
  .scrollIntoView({ behavior: 'smooth', block: 'start' });

Это правильный способ реализовать кнопку «наверх» или плавную навигацию по якорям на странице. Используйте querySelector, чтобы получить целевой элемент.

Событие scroll в JavaScript

Событие scroll срабатывает при прокрутке документа или прокручиваемого элемента. Это одно из наиболее часто используемых событий для создания динамичных и интерактивных интерфейсов.

Ключевые концепции

  • Частота событий: Событие scroll может срабатывать десятки раз в секунду, поэтому его обработчик выполняется очень часто. Выполнение тяжёлых операций (чтение макета, запись в DOM, сетевые запросы) при каждом срабатывании вызывает «дёрганье» при прокрутке. Стандартное решение — throttle или debounce обработчика.
  • Прокрутка window и элемента: Можно слушать прокрутку всего окна (window.addEventListener('scroll', ...)) или конкретного элемента с переполнением (el.addEventListener('scroll', ...)).
  • scroll не всплывает: Событие scroll на элементе не всплывает до документа, поэтому обработчик нужно прикреплять к тому элементу, который фактически прокручивается.

Throttle и Debounce

Оба подхода ограничивают частоту вызовов обработчика, но работают по-разному:

  • Debounce ждёт, пока прокрутка не остановится на wait мс, а затем выполняет обработчик один раз. Подходит для ситуаций «сделать что-то после окончания прокрутки» (например, сохранить позицию прокрутки).
  • Throttle выполняет обработчик не чаще одного раза в wait мс во время прокрутки. Лучше подходит для непрерывных эффектов, таких как индикаторы прогресса, когда нужны регулярные обновления.
function debounce(func, wait) {
  let timeout;
  return function (...args) {
    clearTimeout(timeout);
    timeout = setTimeout(() => func.apply(this, args), wait);
  };
}

function throttle(func, wait) {
  let last = 0;
  return function (...args) {
    const now = Date.now();
    if (now - last >= wait) {
      last = now;
      func.apply(this, args);
    }
  };
}

// Debounce: runs once 100ms after scrolling stops
window.addEventListener('scroll', debounce(() => {
  // handle scroll logic here
}, 100));

// Throttle: runs at most every 100ms while scrolling
window.addEventListener('scroll', throttle(() => {
  // update a progress bar, etc.
}, 100));

Оба вспомогательных метода на основе setTimeout используют API планирования setTimeout / setInterval.

Практические примеры обработки событий прокрутки

Пример 1: Показ/скрытие навигации при прокрутке

Этот пример демонстрирует, как скрывать навигационную панель при прокрутке вниз и показывать при прокрутке вверх — распространённый паттерн на многих современных сайтах для максимального использования экранного пространства.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Scroll Event Navigation Example</title>
    <style>
      #navbar {
        position: fixed;
        top: 0;
        width: 100%;
        background-color: #333;
        color: white;
        text-align: center;
        padding: 10px;
        transition: top 0.3s;
      }
      body {
        padding: 0;
        margin: 0;
        height: 1500px; /* to ensure scrolling */
        font-family: Arial, sans-serif;
      }
    </style>
  </head>
  <body>
    <p style="display: flex; justify-content: center; align-items: center; margin-top: 50vh;"><strong>When you scroll down, the navigation bar disappears. Scroll back up, and it reappears!</strong></p>

    <div id="navbar">Navigation Bar</div>

    <script>
      let lastScrollTop = 0;
      window.addEventListener(
        "scroll",
        function () {
          let currentScroll = window.pageYOffset || document.documentElement.scrollTop;
          if (currentScroll > lastScrollTop) {
            document.getElementById("navbar").style.top = "-50px"; // Adjust based on nav height
          } else {
            document.getElementById("navbar").style.top = "0px";
          }
          lastScrollTop = currentScroll <= 0 ? 0 : currentScroll; // For Mobile or negative scrolling
        },
        false
      );
    </script>
  </body>
</html>

Объяснение:

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

Пример 2: Запуск анимации при прокрутке

Этот пример показывает, как запускать анимации при появлении элементов в viewport во время прокрутки.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8" />
    <title>Scroll Animation Trigger</title>
    <style>
        .box {
            width: 100px;
            height: 100px;
            background: red;
            opacity: 0;
            transition: opacity 2s;
            margin: 600px auto;  /* Ensures it starts out of view */
        }
    </style>
</head>
<body>
<p>Keep scrolling down to see the animation!</p>
<div class="box"></div>

<script>
    let hasAnimated = false;
    window.addEventListener('scroll', function() {
        const box = document.querySelector('.box');
        const rect = box.getBoundingClientRect();
        
        if (rect.top < window.innerHeight && !hasAnimated) {
            box.style.opacity = 1;  // Fade in the box when it comes into view
            hasAnimated = true;
        }
    });
</script>
</body>
</html>

Объяснение:

  • Проверка видимости: Скрипт проверяет, находится ли верхняя граница элемента .box в пределах viewport, и устанавливает его прозрачность в 1, запуская эффект плавного появления. Флаг предотвращает повторный запуск анимации при последующих событиях прокрутки.

Пример 3: Параллакс-эффект прокрутки

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

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8" />
    <title>Enhanced Parallax Scrolling</title>
    <style>
        body, html {
            height: 100%;
            margin: 0;
            font-family: Arial, sans-serif;
            overflow-x: hidden; /* Prevent horizontal scroll */
        }
        .parallax {
            height: 100vh; /* Full height of the viewport */
            position: relative;
            background: url('https://via.placeholder.com/1920x1080') no-repeat center center; 
            background-size: cover;
            display: flex;
            justify-content: center;
            align-items: center;
            color: white;
            font-size: 36px;
            letter-spacing: 1px;
        }
        .content {
            height: 100vh;
            display: flex;
            align-items: center;
            justify-content: center;
            background: white;
            color: #333;
            font-size: 24px;
            padding: 0 20px;
            text-align: center;
            box-sizing: border-box;
            border-top: 1px solid #ccc;
            border-bottom: 1px solid #ccc;
        }
    </style>
</head>
<body>

<div class="content">Scroll down to see the parallax effect!</div>
<div class="parallax">Stunning Parallax!</div>
<div class="content">Keep scrolling to see more effects.</div>
<div class="parallax"></div>
<div class="content">You have reached the end. Amazing, right?</div>

<script>
    document.addEventListener('scroll', function() {
        document.querySelectorAll('.parallax').forEach(function(el) {
            const factor = 0.5; // Change this for more or less parallax
            const offset = window.pageYOffset * factor - 300; // Adjusts the starting position of background
            el.style.backgroundPositionY = offset + 'px';
        });
    });
</script>

</body>
</html>

Объяснение:

  1. Стили CSS: Класс .parallax задаёт фоновое изображение на весь контейнер и центрирует его. Пример полностью полагается на JavaScript для позиционирования, избегая CSS-свойства background-attachment: fixed, которое может вызывать проблемы с производительностью на мобильных устройствах.
  2. Функциональность JavaScript: При прокрутке скрипт вычисляет новую вертикальную позицию фонового изображения на основе смещения прокрутки. Динамически изменяя backgroundPositionY, изображение смещается с иной скоростью, чем контент страницы, создавая эффект глубины параллакса.

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

Современная альтернатива: IntersectionObserver

Для распространённого случая «выполнить действие, когда элемент становится видимым» (ленивая загрузка, анимации появления, бесконечная прокрутка) рекомендуется использовать современный подход с IntersectionObserver. Вместо того чтобы запускать код при каждом событии scroll и вручную считывать позиции, браузер асинхронно сообщает, когда элемент пересекает заданный порог — это значительно эффективнее и не вызывает «дёрганья».

const observer = new IntersectionObserver(
  (entries) => {
    entries.forEach((entry) => {
      if (entry.isIntersecting) {
        entry.target.classList.add('visible'); // run once it enters view
        observer.unobserve(entry.target);      // stop watching it
      }
    });
  },
  { threshold: 0.25 } // fire when 25% of the element is visible
);

document.querySelectorAll('.box').forEach((el) => observer.observe(el));

Используйте событие scroll, когда вам нужен непрерывный поток данных о позиции прокрутки (параллакс, индикаторы прогресса). Используйте IntersectionObserver, когда вас интересует только факт появления или исчезновения элемента в viewport. По концепции он похож на API MutationObserver.

Блокировка и восстановление прокрутки

Распространённое требование пользовательского интерфейса — блокировать прокрутку страницы, пока открыто модальное окно или меню. Надёжно отменить событие scroll с помощью preventDefault() невозможно (оно срабатывает после того, как прокрутка уже произошла). Вместо этого переключайте CSS-свойство overflow:

// Lock scrolling (e.g. when opening a modal)
document.body.style.overflow = 'hidden';

// Restore it when the modal closes
document.body.style.overflow = '';

Заключение

Эффективная обработка событий прокрутки — важный навык для веб-разработчика, позволяющий создавать более интерактивные и производительные сайты. Независимо от того, реализуете ли вы улучшения пользовательского интерфейса, такие как динамические навигационные панели, или добавляете параллакс-эффект на страницу, понимание и правильная обработка прокрутки в JavaScript могут значительно улучшить пользовательский опыт.

Практика

Практика
Какие варианты использования события scroll в JavaScript являются уместными при веб-разработке?
Какие варианты использования события scroll в JavaScript являются уместными при веб-разработке?
Was this page helpful?