概览
用户在与轮播界面或图库等界面元素互动时,希望获得即时的视觉反馈。传统的滚动贴靠仅在滚动手势完成且元素稳定后提供反馈。通过使用滚动贴靠事件(特别是 scrollsnapchanging),您可以在滚动操作期间提供实时反馈,在用户松开触控或鼠标之前突出显示待贴靠的目标。
实现
1. 您可以说scrollsnapchanging
将 scrollsnapchanging 的事件监听器附加到滚动容器。当浏览器确定可能会选择新的贴靠目标时,就会触发此事件。
const container = document.querySelector('#gallery');
const thumbnails = document.querySelectorAll('.thumbnail');
const items = document.querySelectorAll('.gallery-item');
container.addEventListener('scrollsnapchanging', (event) => {
// Highlight pending snap target during scroll for real-time feedback.
const pendingTarget = event.snapTargetInline;
const index = [...items].indexOf(pendingTarget);
if (index === -1 || !thumbnails[index]) return;
// Use lightweight class toggle to avoid layout thrashing during rapid events.
// Note: aria-current is NOT toggled here. It tracks the settled "current"
// item, which is updated in the scrollsnapchange handler below.
thumbnails.forEach((thumb) => thumb.classList.remove('pending'));
thumbnails[index].classList.add('pending');
});
此示例使用 snapTargetInline,因为图库会水平滚动。如果滚动容器垂直滚动,请改用 snapTargetBlock。
2. 您可以说scrollsnapchange
如需在滚动动作完成且元素实际贴靠时最终确定状态,请监听 scrollsnapchange 事件。这是确定最终活跃状态所必需的。
container.addEventListener('scrollsnapchange', (event) => {
// Promote pending state to active on scroll completion.
const snappedTarget = event.snapTargetInline;
const index = [...items].indexOf(snappedTarget);
if (index === -1 || !thumbnails[index]) return;
// Establish final active state and clean up pending.
thumbnails.forEach((thumb) => {
thumb.classList.remove('pending', 'active');
thumb.removeAttribute('aria-current');
});
thumbnails[index].classList.add('active');
thumbnails[index].setAttribute('aria-current', 'true');
});
3. 同步初始状态
网页加载时,浏览器可能会恢复滚动位置(例如,通过历史记录遍历或锚链接)。scrollsnapchange 和 scroll 事件都不会自动触发。运行一次性几何检查,使界面与初始滚动位置保持同步。
// Note: For item.offsetLeft to be relative to the container,
// the container MUST be the offsetParent (e.g., `position: relative`).
const findClosestItemIndex = () => {
// Center-distance assumes scroll-snap-align: center on items.
// For start-aligned snap, compare scrollLeft to item.offsetLeft directly.
const containerCenter = container.scrollLeft + container.clientWidth / 2;
let closestIndex = 0;
let minDistance = Infinity;
items.forEach((item, index) => {
const itemCenter = item.offsetLeft + item.offsetWidth / 2;
const distance = Math.abs(containerCenter - itemCenter);
if (distance < minDistance) {
minDistance = distance;
closestIndex = index;
}
});
return closestIndex;
};
const initActiveItem = () => {
const closestIndex = findClosestItemIndex();
if (!thumbnails[closestIndex]) return;
thumbnails.forEach((thumb) => {
thumb.classList.remove('pending', 'active');
thumb.removeAttribute('aria-current');
});
thumbnails[closestIndex].classList.add('active');
thumbnails[closestIndex].setAttribute('aria-current', 'true');
};
if (document.readyState === 'complete') {
initActiveItem();
} else {
window.addEventListener('load', initActiveItem, { once: true });
}
后备策略
对于不支持 scrollsnapchanging 的浏览器,默认情况下,界面不会在滚动滑动手势期间提供即时反馈,并且关联的界面会与内容不同步。
强制性要求:为不支持此功能的浏览器提供后备方案,否则关联的界面将与内容不同步。
务必使用 scroll 事件监听器以及 requestAnimationFrame 和几何距离计算来模拟 scrollsnapchanging 的实时特性,以便在用户主动滚动时确定最接近的贴靠目标。
if ('onscrollsnapchanging' in Element.prototype) {
// Use native scroll snap events
} else {
// Fallback: use scroll + requestAnimationFrame for eager feedback
// (assumes the same container, thumbnails, items defined in step 1)
let scrollTimeout;
let rafId = null;
const promotePendingToActive = () => {
const closestIndex = findClosestItemIndex();
if (!thumbnails[closestIndex]) return;
thumbnails.forEach((thumb) => {
thumb.classList.remove('pending', 'active');
thumb.removeAttribute('aria-current');
});
thumbnails[closestIndex].classList.add('active');
thumbnails[closestIndex].setAttribute('aria-current', 'true');
};
container.addEventListener('scroll', () => {
if (rafId) return;
rafId = requestAnimationFrame(() => {
rafId = null;
const closestIndex = findClosestItemIndex();
if (!thumbnails[closestIndex]) return;
// DO NOT forget to clean up stale pending classes
thumbnails.forEach((thumb) => thumb.classList.remove('pending'));
thumbnails[closestIndex].classList.add('pending');
});
// Debounce fallback for browsers that don't support scrollend
clearTimeout(scrollTimeout);
scrollTimeout = setTimeout(promotePendingToActive, 100);
}, { passive: true });
// Fallback: use Baseline `scrollend` event to promote pending to active cleanly where supported
container.addEventListener('scrollend', () => {
clearTimeout(scrollTimeout);
promotePendingToActive();
});
}
几何 scroll + requestAnimationFrame 后备方案可密切模拟原生贴靠预测的行为,包括正确处理程序化滚动。由于 scrollIntoView 等函数在执行期间会自然触发 scroll 事件,因此界面将在整个滚动动画中保持流畅同步,而无需额外的自定义逻辑。