scroll-state() do CSS

Como consultas de contêiner, mas para consultas travadas, fixadas e com transbordamento.

Publicado em 15 de janeiro de 2025

O Chrome 133 se baseia nas consultas de contêiner, introduzindo consultas de contêiner de estado de rolagem. O estado gerenciado pelo navegador para posicionamento fixo, pontos de ajuste de rolagem e elementos roláveis agora pode ser consultado e adaptado pelo CSS.

Visão geral

Antes das consultas de estado de rolagem, você precisa usar JavaScript para entender se um elemento está travado, fixado ou rolável. Agora há um método mais eficiente na faixa de padrões para saber essas informações e se adaptar de acordo. Há também uma nova maneira de acionar animações, liberando a animação acionada por rolagem do CSS.

Confira uma visão geral das consultas de estado disponíveis no Chrome 133:

Estado travado:
Aciona mudanças de estilo quando um elemento está preso a uma borda.
Estado fixado:
Aciona mudanças de estilo quando um elemento é fixado em um eixo.
Estado rolável:
Aciona mudanças de estilo quando um elemento está transbordando.

A boa notícia é que tudo o que você aprendeu com as consultas de contêiner vai ajudar você a trabalhar com consultas de estado de rolagem.

Também há um território desconhecido entre as animações de rolagem e as consultas de contêiner de estado de rolagem. Precisamos experimentar o tempo e o contexto para descobrir se uma animação de rolagem ou uma animação de estado de rolagem acionada por rolagem será a melhor. O vídeo e a demonstração a seguir ilustram o problema: uma animação acionada por adesão em comparação com uma animação gerada por rolagem.

(à esquerda) animação acionada por scroll-state(), (à direita) animação orientada por rolagem
https://codepen.io/web-dot-dev/pen/emOrBaV

Primeira consulta de estado de rolagem

A primeira etapa é definir o contêiner usando um novo valor para a propriedade container-type. Assim como em uma consulta de contêiner, o elemento que você quer consultar é aquele que recebe o container-type e, opcionalmente, um container-name. Com as consultas de estado de rolagem, você fornece o container-type: scroll-state do elemento que está fixado, travado ou com transbordamento.

.stuck-top {
  container-type: scroll-state;
  position: sticky;
  top: 0px;
}

A segunda etapa é selecionar o filho desse contêiner que vai responder ao estado. Com as consultas de contêiner, não é possível usar o mesmo elemento que tem o container-type.

.stuck-top {
  container-type: scroll-state;
  position: sticky;
  top: 0px;

  > nav {
    @container scroll-state(stuck: top) {
      background: Highlight;
      color: HighlightText;
    }
  }
}

A terceira etapa é testá-lo. O exemplo de CSS a seguir vai estilizar o plano de fundo em vermelho quando o elemento .stuck-top ficar preso ao topo em 0. Com algumas linhas extras no CSS que já escrevemos e um elemento extra que contém o estado do navegador, nossos componentes são muito mais inteligentes em relação ao que os cerca.

https://codepen.io/web-dot-dev/pen/ByBxpwR

Aprimoramento progressivo

A regra @supports e o aninhamento permitem adicionar o aprimoramento progressivo ou o uso condicional de recursos em apenas algumas linhas de código:

.stuck-top {
  container-type: scroll-state;
  position: sticky;
  top: 0px;

  @supports (container-type: scroll-state) {
    > nav {
      @container scroll-state(stuck: top) {
        background: Highlight;
        color: HighlightText;
      }
    }
  }
}

Além disso, lembre-se de usar @media (prefers-reduced-motion: no-preference) {} em torno do movimento, se você animar elementos na página com consultas de estado de rolagem.

Casos de uso

Preso(a)

Talvez essa seção devesse ser chamada de "situações fixas"? Esta é uma pequena coleção de casos de uso de estado fixo, além de uma seção bônus de ideias que precisam ser criadas.

@container scroll-state(stuck: top) {}
@container scroll-state(stuck: bottom) {}

Lista completa de sintaxe

Adicionar uma sombra quando estiver preso

Um dos casos de uso mais comuns para uma consulta bloqueada é para barras de navegação que querem adicionar box-shadow quando estão bloqueadas, para que possam aparecer flutuando sobre o conteúdo que estão sobrepondo.

https://codepen.io/web-dot-dev/pen/GgKdryj
.stuck-top {
  container-type: scroll-state;
  position: sticky;
  top: 0px;

  > nav {
    transition: box-shadow .3s ease;

    @container scroll-state(stuck: top) {
      box-shadow: var(--shadow-5);
    }
  }
}

Ativar o cabeçalho atual.

Outro cenário comum de feedback de interface fixa é destacar o elemento fixado. Em uma lista de bandas em ordem alfabética, isso pode ser muito útil e contribuir para a experiência.

https://codepen.io/web-dot-dev/pen/pvzVRaK
.sticky-slide {
  dt {
    container-type: scroll-state;
    position: sticky;
    inset-block-start: 0;
    inset-inline: 0;

    > header {
      transition: 
        background .3s ease,
        box-shadow .5s ease;

      @container scroll-state(stuck: top) {
        background: hsl(265 100% 27%);
        box-shadow: 0 5px 5px #0003;
      }
    }
  }
}

Aqui está outra variante, em que os cabeçalhos estão ao lado dos itens da lista. Muitas possibilidades.

https://codepen.io/web-dot-dev/pen/azoGpGg

Estouro de ideias

Confira uma lista de demonstrações fixas que podem inspirar você a dar um toque especial ou remover o JavaScript delas com consultas de estado de rolagem. Sugiro que você tente criar um que você goste. Isso vai ajudar a sintaxe e as ideias a ficarem fixadas 😏.

Capturado

Com as consultas de estado fixado, podemos remover parte da responsabilidade do JavaScript e dos eventos de ajuste e transferir o processamento para o CSS.

@container scroll-state(snapped: x) {}
@container scroll-state(snapped: y) {}
@container scroll-state(snapped: inline) {}
@container scroll-state(snapped: block) {}

Lista completa de sintaxe

Um pequeno lembrete, caso você tenha pulado a seção Primeira consulta de estado de rolagem, o contêiner de uma consulta de ajuste é o elemento com scroll-snap-align, e o elemento que pode se adaptar precisa ser filho desse elemento. Isso significa que há três elementos necessários para configurar isso:

a scroll container with `scroll-snap-type`
⤷ a snap target with both `scroll-snap-align` and `container-type: scroll-state`
    ⤷ a child of the snap target that can query the container for snap state

Melhorar visualmente o item encaixado

É muito comum que um controle deslizante fixado no centro destaque ou mostre o item fixado no centro. Neste exemplo de depoimentos, a palavra-chave not é usada para que todos os depoimentos desativados tenham baixa opacidade, enquanto os ativados permanecem no estado de apresentação natural.

https://codepen.io/web-dot-dev/pen/NPKMdBX
.demo {
  overflow: auto hidden;
  scroll-snap-type: x mandatory;

  > article {
    container-type: scroll-state;
    scroll-snap-align: center;

    @supports (container-type: scroll-state) {
      > * {
        transition: opacity .5s ease;

        @container not scroll-state(snapped: x) {
          opacity: .25;
        }
      }
    }
  }
}

Mostrar a legenda do item fixado

Este é um bom exemplo de como as consultas de estado de rolagem ativam a animação acionada pelo rolagem. É também um bom exemplo de quando respeitar a redução de movimento é importante no CSS.

https://codepen.io/web-dot-dev/pen/XJrqpBG
.demo {
  overflow-x: auto;
  scroll-behavior-x: contain;
  scroll-snap-type: x mandatory;

  > .card {
    container-type: scroll-state;
    scroll-snap-align: center;

    @supports (container-type: scroll-state) {
      @media (prefers-reduced-motion: no-preference) {
        figcaption {
          transform: translateY(100%);

          @container scroll-state(snapped: x) {
            transform: translateY(0);
          }
        }
      }
    }
  }
}

Como animar elementos de slides

É muito comum animar elementos de uma apresentação de slides ou de uma palestra. Era muito chato escrever um observador de interseção para isso, que apenas definia uma classe no slide. Agora não precisamos de JavaScript.

https://codepen.io/web-dot-dev/pen/dPbeNqY
html {
  scroll-snap-type: y mandatory;
}

section {
  container-type: scroll-state;
  scroll-snap-align: start;
  scroll-snap-stop: always;

  @supports (container-type: scroll-state) {
    @media (prefers-reduced-motion: no-preference) {
      > h1 {
        transition: opacity .5s ease, transform .5s var(--ease-spring-3);
        transition-delay: .5s;
        opacity: 0;
        transform: scale(1.25);

        @container scroll-state(snapped: block) {
          opacity: 1;
          transform: scale(1);
        }
      }
    }
  }
}

Todas as consultas de estado do CSS fixadas se comportam como scrollsnapchanging, e não como scrollsnapchange. Isso oferece o gancho mais rápido possível para fornecer feedback visual do elemento fixado. Se ele for muito rápido, considere o evento JavaScript.

Rolável

A consulta de estado rolável será muito útil para mostrar recursos visuais quando uma área de rolagem pode ser rolada. Até as consultas de estado de rolagem, essa era uma informação difícil de saber.

@container scroll-state(scrollable: top) {}
@container scroll-state(scrollable: right) {}
@container scroll-state(scrollable: bottom) {}
@container scroll-state(scrollable: left) {}

Lista completa de sintaxe

Indicar rolagem com sombras

Há um famoso truque de CSS de Lea Verou que usa background-attachment: local para conseguir um efeito semelhante a esse, além de uma maneira de fazer isso com animação orientada por rolagem. Cada técnica tem suas vantagens e desvantagens. Cabe a nós descobrir quando e onde cada uma delas é mais adequada.

O exemplo a seguir usa um único elemento fixo que abrange o scrollport. Um gradiente na parte de cima e outro na parte de baixo têm a opacidade animada com @property quando a consulta de estado de rolagem contextual é aplicada: @container scroll-state(scrollable: top).

Ele é o primeiro contêiner que é size e scroll-state.

https://codepen.io/web-dot-dev/pen/OPLZWBj
.scroll-container {
  container-type: scroll-state size;
  overflow: auto;

  &::after {
    content: " ";

    background: var(--_shadow-top), var(--_shadow-bottom);
    transition: 
      --_scroll-shadow-color-1-opacity .5s ease,
      --_scroll-shadow-color-2-opacity .5s ease;

    @container scroll-state(scrollable: top) {
      --_scroll-shadow-color-1-opacity: var(--_shadow-color-opacity, 25%);
    }

    @container scroll-state(scrollable: bottom) {
      --_scroll-shadow-color-2-opacity: var(--_shadow-color-opacity, 25%);
    }
  }
}

Comando de seta

Às vezes, mostrar uma seta pode ajudar os usuários a descobrir que uma área pode ser rolada. Elas tendem a apontar para a direção em que a rolagem pode ocorrer e desaparecem quando não são mais necessárias. Você pode fazer isso com o seguinte código.

https://codepen.io/web-dot-dev/pen/OPLZWBj
@container scroll-state((scrollable: top) or (not (scrollable: bottom))) {
  translate: 0 calc(100% + 10px);
}

@container scroll-state((scrollable: top) and (not (scrollable: bottom))) {
  translate: 0 calc(100% + 10px);
  rotate: .5turn;
}

Voltar ao início

Outra interação de estado de rolagem comum é o botão de conveniência "rolar para cima". O código a seguir faz com que o botão de rolagem para cima desapareça quando não há mais nada para rolar para cima.

Essa solução é um pouco invertida, mas permite reduzir a quantidade de CSS. O botão fica visível quando está em repouso, então você precisa dizer a ele para se esconder quando não houver mais para onde rolar para cima.

https://codepen.io/web-dot-dev/pen/OPLZWBj
@container not scroll-state(scrollable: top) {
  translate: 0 calc(100% + 10px);
}

Continuação do estudo

Se você quiser mais informações, confira alguns recursos que vão desde detalhes de especificações até outros ótimos artigos sobre o assunto: