Saat men-scroll secara terprogram ke bagian atau elemen (terutama dengan behavior: 'smooth'), memicu tindakan lanjutan segera—seperti mengedipkan status sorotan, memulai animasi masuk, atau memindahkan fokus—akan menyebabkan efek berjalan saat target masih di luar layar atau di tengah scroll.
Sebelumnya, developer harus menebak durasi scroll dengan setTimeout() (yang diaktifkan terlalu awal pada scroll panjang dan terlalu lambat pada scroll pendek) atau menggabungkan pemroses peristiwa scrollend secara manual dalam Promise dengan timer penggantian (karena scrollend tidak diaktifkan saat target sudah terlihat dan tidak ada scroll yang terjadi).
Metode scroll terprogram di Element (scrollIntoView(), scroll(), scrollTo(), scrollBy()) dan Window (scroll(), scrollTo(), scrollBy()) menampilkan Promise yang di-resolve saat operasi scroll selesai. Jika target sudah berada di posisi scroll yang diminta dan tidak perlu di-scroll, Promise yang ditampilkan akan segera di-resolve tanpa hang.
Penerapan
- Tandai pengendali sebagai
async: Gunakan fungsiasyncuntuk pemroses peristiwa atau pengendali navigasi yang memicu scroll. - Menunggu metode scroll secara langsung: Panggil
await element.scrollIntoView({ behavior: 'smooth', ... })(atauawait element.scrollTo(...),await window.scrollTo(...), dll.). Eksekusi dijeda hingga browser menyelesaikan animasi scroll dan posisi scroll ditetapkan. - Terapkan efek pasca-scroll: Alihkan class sorotan, mulai animasi, atau pindahkan fokus setelah ekspresi
awaitselesai.
Scroll Terganggu & Pengelolaan Fokus
- Menangani scroll cepat/terganggu: Jika scroll baru dimulai pada penampung scroll yang sama sebelum scroll halus yang sedang berlangsung selesai, browser akan membatalkan scroll halus yang sedang berlangsung dan menyelesaikan (tidak menolak) semua promise scroll yang tertunda di penampung tersebut. Jika Anda perlu menyembunyikan efek pasca-scroll untuk scroll yang digantikan saat pengguna mengklik dengan cepat, lacak ID operasi yang bertambah dan kembali lebih awal setelah
awaitjika scroll yang lebih baru dimulai:
let currentScrollId = 0;
const sections = document.querySelectorAll('section');
async function scrollToAndHighlight(targetElement) {
const scrollId = ++currentScrollId;
sections.forEach((sec) => sec.classList.remove('highlight'));
await targetElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
// Abort follow-up effect if another scroll was triggered while awaiting
if (scrollId !== currentScrollId) return;
targetElement.classList.add('highlight');
}
- Memfokuskan target setelah men-scroll: Saat memindahkan fokus keyboard ke elemen target (atau judul di dalamnya dengan
tabindex="-1") setelahawait targetElement.scrollIntoView(...), teruskan{ preventScroll: true }kefocus()agar panggilan fokus tidak memulai scroll kedua yang bersaing:
await targetElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
targetHeading.focus({ preventScroll: true });
Batasan & Praktik Terbaik
- WAJIB: Tunggu
Promiseyang ditampilkan langsung olehscrollIntoView(),scroll(),scrollTo(), atauscrollBy(), bukan menebak durasi scroll dengansetTimeout(). - JANGAN menggabungkan
scrollenddalamnew Promise()manual sebagai implementasi utama Anda—scrollendtidak diaktifkan saat target sudah terlihat dan tidak ada jarak scroll yang ditempuh, sedangkan promise metode scroll akan otomatis di-resolve dalam kasus tersebut. - LAKUKAN: Teruskan
{ preventScroll: true }saat memanggil.focus()setelah menunggu metode scroll agar browser tidak melompat atau men-scroll untuk kedua kalinya. - LAKUKAN: Lindungi efek pasca-scroll dengan token permintaan jika interaksi pengguna yang cepat dapat memicu scroll halus yang tumpang-tindih pada penampung yang sama, karena menginterupsi scroll halus akan menyelesaikan (bukan menolak) promise sebelumnya.
- Bug Chrome umum dengan root scroller: Chrome memiliki bug yang menyebabkan
Promiseyang ditampilkan olehscrollIntoView()di-resolve terlalu awal saat root scroller dokumen di-scroll. MenungguscrollIntoView()pada penampung scroll bertingkat berfungsi seperti yang diharapkan. Jangan gabungkan konten Anda dalam penampung scroll, sepertidivdenganoverflow-y: auto, sebagai solusi untuk bug ini.
Strategi penggantian
Di browser yang tidak mendukung promise metode scroll, scrollIntoView(), scroll(), scrollTo(), dan scrollBy() menampilkan undefined, bukan Promise.
- Progressive enhancement (perilaku
awaitdefault): Karenaawait undefinedadalah JavaScript valid yang langsung di-resolve pada microtask berikutnya, penulisanawait element.scrollIntoView({ behavior: 'smooth' })tidak akan menampilkan error di browser yang tidak didukung. Sebagai gantinya, browser akan menurun secara halus dengan menjalankan kode pasca-scroll segera setelah scroll dimulai. Untuk hiasan visual yang tidak penting seperti penyorotan sementara, hal ini sering kali sudah cukup. - Penggantian yang akurat (
scrollend+ waktu tunggu): Jika target Dasar Pengukuran Anda tidak mendukungscroll-promisesdan tindakan setelah scroll harus menunggu hingga scroll selesai di browser lama, periksa apakah nilai yang ditampilkan adalahPromise(result instanceof Promise). Jika nilainyaundefined, tunggu peristiwascrollenddi penampung scroll dengan waktu tunggu penggantian singkat jika elemen sudah terlihat dan tidak ada scroll yang terjadi:
function scrollIntoViewAsync(element, options, scroller = window) {
const result = element.scrollIntoView(options);
// Feature detect: in supporting browsers, scrollIntoView() returns a Promise
if (result instanceof Promise) {
return result;
}
// Fallback for browsers where scrollIntoView() returns undefined
return new Promise((resolve) => {
let timeoutId;
const finish = () => {
clearTimeout(timeoutId);
scroller.removeEventListener('scrollend', finish);
scroller.removeEventListener('scroll', onScroll);
resolve();
};
const onScroll = () => {
// Restart the idle timer on every scroll event. If scrollend never fires
// (browsers without scrollend support), this resolves once scrolling goes idle.
clearTimeout(timeoutId);
timeoutId = setTimeout(finish, 100);
};
scroller.addEventListener('scrollend', finish, { once: true });
scroller.addEventListener('scroll', onScroll, { passive: true });
// If no scroll event fires within the idle window (e.g., already in view), resolve.
// 100ms is an example value; it only needs to exceed the gap between scroll events.
timeoutId = setTimeout(finish, 100);
});
}