Khái niệm
Sự kiện scroll kích hoạt khi người dùng cuộn nội dung (window hoặc element có overflow). Có thể dùng để: lazy load, infinite scroll, parallax, sticky header, progress bar.
Ví dụ cơ bản
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Scroll Events</title>
<style>
body { font-family: 'Segoe UI', sans-serif; padding: 20px; height: 300vh; } /* Tạo scrollbar */
.result {
position: fixed; top: 20px; right: 20px;
font-size: 16px; font-weight: 500; color: #6a1b9a;
padding: 15px; background: #f3e5f5; border-radius: 4px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1); z-index: 1000;
min-width: 250px;
}
h3 { position: fixed; top: 100px; right: 20px; color: #555; z-index: 1000; }
.content { margin-top: 150px; }
.section { height: 80vh; display: flex; align-items: center; justify-content: center; font-size: 32px; font-weight: bold; color: white; }
.s1 { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }
.s2 { background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); }
.s3 { background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); }
</style>
</head>
<body>
<h1>Scroll Events trong JavaScript</h1>
<div class="result" id="result">Cuộn trang để xem thông tin...</div>
<h3>Cuộn thanh cuộn để thấy thông tin cập nhật</h3>
<div class="content">
<div class="section s1">Section 1</div>
<div class="section s2">Section 2</div>
<div class="section s3">Section 3</div>
</div>
<script>
const resEle = document.getElementById('result');
// Throttle để tối ưu performance
let ticking = false;
window.addEventListener('scroll', () => {
if (!ticking) {
window.requestAnimationFrame(() => {
updateScrollInfo();
ticking = false;
});
ticking = true;
}
}, { passive: true }); // Passive listener cho performance
function updateScrollInfo() {
const scrollY = window.scrollY || window.pageYOffset;
const docHeight = document.documentElement.scrollHeight - window.innerHeight;
const scrollPercent = ((scrollY / docHeight) * 100).toFixed(1);
resEle.innerHTML = `
📍 Scroll Y: ${scrollY}px
📏 Trang cao: ${docHeight}px
📊 Tiến độ: ${scrollPercent}%
🎯 document.scrollingElement: ${document.scrollingElement?.tagName}
`;
}
</script>
</body>
</html>
Kết quả
Khi cuộn: Hiển thị vị trí scrollY, chiều cao trang, % tiến độ cập nhật realtime.
Tối ưu Performance (Quan trọng)
1. Passive Event Listener
// Cho phép browser scroll ngay mà không chờ preventDefault
window.addEventListener('scroll', handler, { passive: true });
2. Throttle / Debounce
// Throttle với requestAnimationFrame (mượt nhất)
let ticking = false;
window.addEventListener('scroll', () => {
if (!ticking) {
requestAnimationFrame(() => {
handleScroll();
ticking = false;
});
ticking = true;
}
});
// Hoặc dùng lodash throttle
// import { throttle } from 'lodash';
// window.addEventListener('scroll', throttle(handleScroll, 16)); // ~60fps
3. Intersection Observer (Thay thế scroll cho nhiều trường hợp)
// Lazy load images
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
observer.unobserve(img);
}
});
});
document.querySelectorAll('img[data-src]').forEach(img => observer.observe(img));
Ứng dụng thực tế
1. Sticky Header
const header = document.querySelector('header');
const headerHeight = header.offsetHeight;
window.addEventListener('scroll', () => {
header.classList.toggle('sticky', window.scrollY > headerHeight);
});
2. Scroll Progress Bar
const progressBar = document.querySelector('.progress-bar');
window.addEventListener('scroll', () => {
const scrolled = (window.scrollY / (document.body.scrollHeight - window.innerHeight)) * 100;
progressBar.style.width = `${scrolled}%`;
});
3. Back to Top Button
const btn = document.getElementById('backToTop');
window.addEventListener('scroll', () => {
btn.classList.toggle('show', window.scrollY > 300);
});
btn.addEventListener('click', () => {
window.scrollTo({ top: 0, behavior: 'smooth' });
});
4. Infinite Scroll
let loading = false;
window.addEventListener('scroll', async () => {
if (loading) return;
const { scrollTop, scrollHeight, clientHeight } = document.documentElement;
if (scrollTop + clientHeight >= scrollHeight - 500) {
loading = true;
await loadMoreData();
loading = false;
}
});