Прокрутка в 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>Объяснение:
- Стили CSS: Класс
.parallaxзадаёт фоновое изображение на весь контейнер и центрирует его. Пример полностью полагается на JavaScript для позиционирования, избегая CSS-свойстваbackground-attachment: fixed, которое может вызывать проблемы с производительностью на мобильных устройствах. - Функциональность 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 могут значительно улучшить пользовательский опыт.