總覽
使用者與輪轉介面或圖片庫等 UI 元素互動時,會預期立即看到視覺回饋。傳統的捲動貼齊功能只會在捲動手勢完成且元素固定後提供回饋。使用捲動貼齊事件 (特別是 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 事件都不會自動觸發。執行一次性幾何圖形檢查,將 UI 與初始捲動位置同步。
// 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,UI 預設不會在捲動手勢期間提供即時回饋,且連結的 UI 會與內容不同步。
必要:為不支援的瀏覽器提供備用方案,否則連結的 UI 會與內容不同步。
務必使用 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 事件,因此 UI 在捲動動畫期間會保持流暢同步,不需要額外的自訂邏輯。