Tính năng "Pull to Refresh" (kéo xuống để làm mới) phổ biến trên app native, có thể triển khai trên mobile web bằng JavaScript + Touch Events.
Cơ chế hoạt động
- Lắng nghe
touchstart,touchmove,touchendtrên container cuộn. - Khi cuộn lên trên cùng (
scrollTop === 0) và kéo tiếp xuống (touchmoveY tăng) → hiển thị indicator "Kéo để làm mới". - Khi thả (
touchend) nếu kéo đủ ngưỡng → trigger callback (gọi API, reload data). - Animation mượt: CSS transform/transition cho indicator.
Triển khai đơn giản (Vanilla JS)
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
.pull-refresh {
position: fixed;
top: -60px;
left: 0;
right: 0;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
background: #f5f5f5;
border-bottom: 1px solid #ddd;
transition: transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
z-index: 100;
font-size: 14px;
color: #666;
}
.pull-refresh.visible { transform: translateY(60px); }
.pull-refresh.loading { color: #007bff; }
.content { padding-top: 60px; min-height: 100vh; }
</style>
</head>
<body>
<div class="pull-refresh" id="pullRefresh">Kéo xuống để làm mới</div>
<div class="content" id="scrollContainer">
<!-- Nội dung dài -->
<div style="height: 2000px; padding: 20px;">Nội dung trang...</div>
</div>
<script>
var pullRefresh = document.getElementById('pullRefresh');
var container = document.getElementById('scrollContainer');
var startY = 0;
var pulling = false;
var threshold = 60; // px
container.addEventListener('touchstart', function(e) {
if (container.scrollTop === 0) {
startY = e.touches[0].clientY;
pulling = true;
}
}, { passive: true });
container.addEventListener('touchmove', function(e) {
if (!pulling) return;
var deltaY = e.touches[0].clientY - startY;
if (deltaY > 0) {
e.preventDefault(); // Ngăn scroll mặc định
var pullDistance = Math.min(deltaY * 0.5, threshold * 1.5); // Resistance
pullRefresh.style.transform = 'translateY(' + (-threshold + pullDistance) + 'px)';
pullRefresh.classList.toggle('visible', pullDistance > threshold * 0.5);
pullRefresh.textContent = pullDistance > threshold ? 'Thả để làm mới' : 'Kéo xuống để làm mới';
}
}, { passive: false });
container.addEventListener('touchend', function() {
if (!pulling) return;
pulling = false;
var currentTransform = pullRefresh.style.transform;
var pulled = currentTransform ? parseFloat(currentTransform.split('(')[1]) : 0;
if (pulled >= 0) { // Kéo đủ ngưỡng
pullRefresh.classList.add('loading');
pullRefresh.textContent = 'Đang làm mới...';
pullRefresh.style.transform = 'translateY(0)';
// Gọi API reload data
setTimeout(function() {
// location.reload(); // Hoặc fetch data mới
pullRefresh.classList.remove('visible', 'loading');
pullRefresh.style.transform = 'translateY(-60px)';
pullRefresh.textContent = 'Kéo xuống để làm mới';
}, 1500);
} else {
// Quay về vị trí ẩn
pullRefresh.style.transform = 'translateY(-60px)';
pullRefresh.classList.remove('visible');
}
});
</script>
</body>
</html>
Thư viện sẵn có (Khuyên dùng Production)
- PullToRefresh.js - Nhẹ, không dependency, customizable.
- iscroll - Scroll engine có sẵn pull-to-refresh (Twitter từng dùng).
- Swiper.js - Slider có built-in pull-to-refresh.
- Framework7, Ionic - UI framework mobile có sẵn component.
Lưu ý UX
- Chỉ bật trên trang danh sách (list/feed), không phải trang chi tiết/form.
- Indicator rõ ràng: icon mũi tên xoay, text hướng dẫn, loading spinner.
- Haptic feedback (navigator.vibrate) khi đạt ngưỡng (nếu hỗ trợ).
- Accessibility: ARIA live region cho screen reader.
Native Browser Pull-to-Refresh
Chrome Android / Safari iOS có pull-to-refresh mặc định trên body. Để tắt (xem bài "Vô hiệu hóa pull-to-refresh Chrome Android"):
body { overscroll-behavior-y: contain; }