Controle sua rolagem, personalizando efeitos de puxar para atualizar e estourar

Texto longo, leia o resumo

A propriedade CSS overscroll-behavior permite que os desenvolvedores substituam o comportamento padrão de rolagem de estouro do navegador ao chegar ao topo/fim do conteúdo. Os casos de uso incluem desativar o recurso de arrastar para baixo para atualizar em dispositivos móveis, remover os efeitos de brilho de rolagem esticada e de elástico e impedir que o conteúdo da página seja rolado quando estiver abaixo de um modal/sobreposição.

Contexto

Limites de rolagem e encadeamento de rolagem

Encadeamento de rolagem no Chrome para Android.

A rolagem é uma das formas mais fundamentais de interagir com uma página, mas alguns padrões de UX podem ser difíceis de lidar devido aos comportamentos padrão peculiares do navegador. Por exemplo, considere uma gaveta de apps com um grande número de itens que o usuário pode precisar rolar. Quando eles chegam ao final, o contêiner de overflow para de rolar porque não há mais conteúdo para consumir. Em outras palavras, o usuário atinge um "limite de rolagem". Mas observe o que acontece se o usuário continuar rolando. O conteúdo atrás da gaveta começa a rolar. A rolagem é assumida pelo contêiner pai, que é a própria página principal no exemplo.

Esse comportamento é chamado de encadeamento de rolagem, o comportamento padrão do navegador ao rolar o conteúdo. Muitas vezes, o padrão é muito bom, mas às vezes não é desejável ou até mesmo inesperado. Alguns apps podem oferecer uma experiência do usuário diferente quando o usuário atinge um limite de rolagem.

O efeito de arrastar para baixo para atualizar

O gesto de arrastar para baixo para atualizar é um gesto intuitivo popularizado por apps para dispositivos móveis, como o Facebook e o Twitter. Puxar para baixo em um feed de rede social e soltar cria um novo espaço para que as postagens mais recentes sejam carregadas. Na verdade, essa UX específica se tornou tão popular que navegadores para dispositivos móveis, como o Chrome no Android, adotaram o mesmo efeito. Deslizar para baixo na parte de cima da página atualiza toda a página:

O recurso personalizado de arrastar para baixo para atualizar do Twitter
ao atualizar um feed no PWA.
A ação nativa de arrastar para baixo para atualizar do Chrome para Android
atualiza a página inteira.

Para situações como o PWA do Twitter, pode ser interessante desativar a ação nativa de arrastar para baixo para atualizar. Por quê? Nesse app, você provavelmente não quer que o usuário atualize a página acidentalmente. Também é possível que uma animação de atualização dupla seja mostrada. Como alternativa, pode ser melhor personalizar a ação do navegador, alinhando-a mais à marca do site. A parte ruim é que esse tipo de personalização é difícil de fazer. Os desenvolvedores acabam escrevendo JavaScript desnecessário, adicionando listeners de toque não passivos (que bloqueiam a rolagem) ou colocando a página inteira em um <div> de 100vw/vh (para evitar que a página transborde). Essas soluções alternativas têm efeitos negativos bem documentados no desempenho da rolagem.

Podemos fazer melhor!

Conheça o overscroll-behavior

A propriedade overscroll-behavior é um novo recurso de CSS que controla o comportamento do que acontece quando você rola um contêiner além do limite (incluindo a própria página). É possível usá-la para cancelar o encadeamento de rolagem, desativar/personalizar a ação de arrastar para baixo para atualizar, desativar os efeitos de elástico no iOS (quando o Safari implementar overscroll-behavior) e muito mais. A melhor parte é que o uso de overscroll-behavior não afeta negativamente o desempenho da página, como os hacks mencionados na introdução.

A propriedade usa três valores possíveis:

  1. auto: padrão. As rolagens que se originam no elemento podem se propagar para elementos ancestrais.
  2. contain: impede o encadeamento de rolagem. As rolagens não se propagam para os ancestrais, mas os efeitos locais no nó são mostrados. Por exemplo, o efeito de brilho da rolagem esticada no Android ou o efeito de elástico no iOS, que notifica o usuário quando ele atinge um limite de rolagem. Observação: o uso de overscroll-behavior: contain no elemento html impede ações de navegação de rolagem esticada.
  3. none: igual a contain, mas também impede efeitos de rolagem esticada no próprio nó (por exemplo, o brilho de rolagem esticada do Android ou o efeito de elástico do iOS).

Vamos analisar alguns exemplos para entender como usar o overscroll-behavior.

Impedir que as rolagens escapem de um elemento de posição fixa

O cenário da caixa de chat

O conteúdo abaixo da janela de chat também rola :(

Considere uma caixa de chat com posição fixa na parte de baixo da página. A intenção é que a caixa de chat seja um componente independente e que role separadamente do conteúdo por trás dela. No entanto, devido ao encadeamento de rolagem, o documento começa a rolar assim que o usuário chega à última mensagem no histórico de chat.

Para esse app, é mais apropriado que as rolagens que se originam na caixa de chat permaneçam no chat. Para isso, adicione overscroll-behavior: contain ao elemento que contém as mensagens de chat:

#chat .msgs {
  overflow: auto;
  overscroll-behavior: contain;
  height: 300px;
}

Basicamente, estamos criando uma separação lógica entre o contexto de rolagem da caixa de chat e a página principal. O resultado final é que a página principal permanece no lugar quando o usuário chega ao topo/fim do histórico de chat. As rolagens que começam na caixa de chat não se propagam para fora.

O cenário de sobreposição de página

Outra variação do cenário de "rolagem insuficiente" é quando você vê o conteúdo rolando atrás de uma sobreposição de posição fixa. É um sinal claro de que overscroll-behavior está em ordem! O navegador está tentando ajudar, mas acaba fazendo o site parecer com bugs.

Exemplo: modal com e sem overscroll-behavior: contain:

Antes: o conteúdo da página é rolado abaixo da sobreposição.
Depois: o conteúdo da página não rola por baixo da sobreposição.

Como desativar o recurso de arrastar para baixo para atualizar

Desativar a ação de arrastar para baixo para atualizar é uma única linha de CSS. Basta impedir o encadeamento de rolagem em todo o elemento que define a janela de visualização. Na maioria dos casos, é <html> ou <body>:

body {
  /* Disables pull-to-refresh but allows overscroll glow effects. */
  overscroll-behavior-y: contain;
}

Com essa simples adição, corrigimos as animações duplas de arrastar para baixo para atualizar na demonstração da caixa de chat e, em vez disso, podemos implementar um efeito personalizado que usa uma animação de carregamento mais organizada. Toda a caixa de entrada também fica desfocada enquanto é atualizada:

Antes
Depois

Confira um snippet do código completo:

<style>
  body.refreshing #inbox {
    filter: blur(1px);
    touch-action: none; /* prevent scrolling */
  }
  body.refreshing .refresher {
    transform: translate3d(0,150%,0) scale(1);
    z-index: 1;
  }
  .refresher {
    --refresh-width: 55px;
    pointer-events: none;
    width: var(--refresh-width);
    height: var(--refresh-width);
    border-radius: 50%;
    position: absolute;
    transition: all 300ms cubic-bezier(0,0,0.2,1);
    will-change: transform, opacity;
    ...
  }
</style>

<div class="refre>she<r"
  div class=&qu><ot;l>oad<ing-bar"/div
  div>< cla>ss=<"loading-bar"></div>
  <div class="loading><-bar>&<quot>;/<div
  div class=&q><uot;loading><-bar&quo>t;</div
/>div

section id="inbox"!-- msgs --/section

script
  let _startY;
  const inbox = document.querySel>ector('#inbox');

  inbox.addEventListener('touchstart', e = {
    _startY = e.touches>[0].pageY;
  }, {passive: true});

  inbox.addEventListener('touchmove', e = {
    const y = e.touches[0].pageY;
    // Activate custom pull-to-refresh effects when at the top of the container&&
  >  // and &&user is scrolling up.
    if (document.scrollingElement.scrollTop === 0  y  _startY 
        !document.body.class<List.co>ntains('refreshing')) {
      // refresh inbox.
    }
  }, {passive: true});
/script

Como desativar os efeitos de brilho e de elástico da rolagem esticada

Para desativar o efeito de retorno ao atingir um limite de rolagem, use overscroll-behavior-y: none:

body {
  /* Disables pull-to-refresh and overscroll glow effect.
     Still keeps swipe navigations. */
  overscroll-behavior-y: none;
}
Antes: ao atingir o limite de rolagem, um brilho é mostrado.
Depois: brilho desativado.

Demonstração completa

Para resumir, a demonstração completa da caixa de chat usa overscroll-behavior para criar uma animação personalizada de arrastar para baixo para atualizar e impedir que as rolagens saiam do widget da caixa de chat. Isso oferece uma experiência do usuário ideal que seria difícil de alcançar sem o CSS overscroll-behavior.

Ver demonstração | Origem