Ao rolar de forma programática para uma seção ou elemento (especialmente com behavior: 'smooth'), acionar uma ação de acompanhamento imediatamente, como piscar um estado de destaque, iniciar uma animação de entrada ou mover o foco, faz com que o efeito seja executado enquanto o destino ainda está fora da tela ou no meio da rolagem.
Antes, os desenvolvedores precisavam adivinhar a duração da rolagem com setTimeout() (que é acionado muito cedo em rolagens longas e muito tarde em rolagens curtas) ou encapsular manualmente um listener de eventos scrollend em um Promise com um timer de fallback (porque scrollend não é acionado quando o destino já está visível e não há rolagem).
Os métodos de rolagem programática em Element (scrollIntoView(), scroll(), scrollTo(), scrollBy()) e Window (scroll(), scrollTo(), scrollBy()) retornam um Promise que é resolvido quando a operação de rolagem é concluída. Se o destino já estiver na posição de rolagem solicitada e nenhuma rolagem for necessária, o Promise retornado será resolvido imediatamente sem travamentos.
Implementação
- Marque o gerenciador como
async: use uma funçãoasyncpara o listener de eventos ou o gerenciador de navegação que aciona a rolagem. - Aguarde o método de rolagem diretamente: chame
await element.scrollIntoView({ behavior: 'smooth', ... })(ouawait element.scrollTo(...),await window.scrollTo(...)etc.). A execução é pausada até que o navegador termine a animação de rolagem e a posição de rolagem seja definida. - Aplicar o efeito pós-rolagem: alterne a classe de destaque, inicie a animação ou mova o foco depois que a expressão
awaitfor concluída.
Rolagens interrompidas e gerenciamento de foco
- Como lidar com rolagens rápidas / interrompidas: se uma nova rolagem começar no mesmo contêiner antes que uma rolagem suave em andamento termine, o navegador vai cancelar a rolagem suave em andamento e resolver (não rejeitar) todas as promessas de rolagem pendentes nesse contêiner. Se você precisar suprimir o efeito pós-rolagem para rolagens substituídas quando um usuário clicar rapidamente, rastreie um ID de operação de incremento e retorne logo após
awaitse uma nova rolagem tiver sido iniciada:
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');
}
- Focar o destino após a rolagem: ao mover o foco do teclado para o elemento de destino (ou um título dentro dele com
tabindex="-1") apósawait targetElement.scrollIntoView(...), transmita{ preventScroll: true }parafocus()para que a chamada de foco não inicie uma segunda rolagem concorrente:
await targetElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
targetHeading.focus({ preventScroll: true });
Restrições e práticas recomendadas
- OBRIGATÓRIO: aguarde o
Promiseretornado diretamente porscrollIntoView(),scroll(),scrollTo()ouscrollBy()em vez de adivinhar as durações de rolagem comsetTimeout(). - NÃO envolva
scrollendem umnew Promise()manual como sua implementação principal. Oscrollendnão é acionado quando o destino já está visível e nenhuma distância de rolagem é percorrida, enquanto as promessas do método de rolagem são resolvidas automaticamente nesse caso. - RECOMENDADO: transmita
{ preventScroll: true }ao chamar.focus()depois de aguardar um método de rolagem para que o navegador não pule ou role uma segunda vez. - RECOMENDAÇÃO: proteja os efeitos pós-rolagem com um token de solicitação se interações rápidas do usuário puderem acionar rolagens suaves sobrepostas no mesmo contêiner, já que interromper uma rolagem suave resolve (em vez de rejeitar) a promessa anterior.
- Bug conhecido do Chrome com o rolador raiz: o Chrome tem um bug em que a
Promiseretornada porscrollIntoView()é resolvida muito cedo quando o rolador raiz do documento está rolando. Aguardar umscrollIntoView()em um contêiner de rolagem aninhado funciona conforme o esperado. Não envolva o conteúdo em um contêiner de rolagem, comodivcomoverflow-y: auto, como uma solução alternativa para esse bug.
Estratégias de fallback
Em navegadores que não são compatíveis com promessas de método de rolagem, scrollIntoView(), scroll(), scrollTo() e scrollBy() retornam undefined em vez de Promise.
- Aprimoramento progressivo (comportamento padrão de
await): comoawait undefinedé um JavaScript válido que é resolvido imediatamente na próxima microtarefa, escreverawait element.scrollIntoView({ behavior: 'smooth' })não vai gerar um erro em navegadores sem suporte. Em vez disso, ele se degrada de forma otimizada executando o código pós-rolagem assim que a rolagem começa. Para detalhes visuais não críticos, como um destaque temporário, isso geralmente é suficiente. - Fallback fiel (
scrollend+ tempo limite): se o destino do Baseline não for compatível comscroll-promisese a ação pós-rolagem precisar esperar até que a rolagem termine em navegadores mais antigos, verifique se o valor de retorno é umPromise(result instanceof Promise). Quando forundefined, aguarde o eventoscrollendno contêiner de rolagem com um tempo limite de fallback curto, caso o elemento já esteja visível e nenhuma rolagem ocorra:
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);
});
}