Layout shift causado por transições CSS

Saiba como encontrar e remover transições CSS que criam layout shifts

Arjen Karel Core Web Vitals Consultant
Arjen Karel - linkedin
Last update: 2026-10-17

Por que transições CSS causam layout shifts

Cumulative Layout Shifts causados por transições CSS costumam acontecer cedo, durante a fase de carregamento da página. Uma transição em width ou height anima a mudança de tamanho quando um stylesheet ou script atrasado altera o estilo, e o conteúdo abaixo se move junto. Esses shifts não acontecem em toda visualização de página. Isso dificulta o debug.

Última revisão por Arjen Karel em março de 2026

O problema do transition: all

A causa que mais encontro é o transition: all. Ele está em todo lugar. Segundo o capítulo de CSS do Web Almanac de 2022, 53% das páginas o usam, mais do que qualquer outra propriedade de transição. Com isso, width e height também são animados e movem o conteúdo ao redor do elemento.

Uma transição CSS tem quatro partes: propriedade, duração, timing function e delay. No shorthand:

/* property | duration | timing-function | delay */
transition: margin-right 4s ease-in-out 1s;

O problema começa com transition: all .3s ease em um elemento onde o desenvolvedor só queria suavizar a cor no hover.

Uma transição nunca roda no primeiro estilo que o navegador calcula para um elemento. Ela só roda quando esse estilo muda depois — e, durante o carregamento da página, isso acontece direto: um stylesheet que chega tarde ou um script que adiciona uma classe ao menu de navegação. Com transition: all, o navegador também anima a mudança de tamanho. O menu acima da dobra se expande durante 300 milissegundos e empurra tudo o que estiver abaixo. Um layout shift que só acontece durante o carregamento. Boa sorte para reproduzir.

Quais propriedades CSS causam layout shift?

Nem toda transição CSS causa layout shifts. Apenas propriedades que alteram o tamanho ou a posição de um elemento fazem o navegador rodar o layout de novo — e só o layout move o conteúdo ao redor. Segundo pesquisas do Google, páginas que animam propriedades CSS que acionam layout têm 15% menos chance de passar no CLS. Margin e border-width são ainda piores: páginas que animam essas propriedades têm CLS ruim com quase o dobro da frequência normal.

Cada uma dessas propriedades aciona o recálculo de layout. Aplicar transição em qualquer uma delas pode deslocar o próprio elemento ou o conteúdo ao redor: width, height, margin, padding, top, left, right, bottom, border-width, font-size.

Seguras para o CLS: transform e opacity podem rodar no compositor. Filter e clip-path custam, no máximo, um repaint. Nenhum dos pares aciona layout.

Apenas paint, logo também seguras para o CLS: background-color, color, box-shadow, outline.

Transform e opacity são as duas que importam. O transform não muda a posição do elemento no layout, então o CLS nunca conta isso como layout shift. Precisa mover um elemento? Use transform: translate() em vez de top e left. Para fazê-lo parecer maior ou menor, use transform: scale() em vez de width e height.

Hover também não é desculpa. O CLS ignora shifts nos 500 ms após um clique, um toque ou o pressionamento de uma tecla. Mas passar o mouse sobre um elemento não entra nessa regra — então uma transição de hover em width conta integralmente.

O vídeo abaixo mostra uma transição deslocando o layout durante a fase de carregamento. Vejo isso o tempo todo. Identificar a causa exige investigar um pouco.

Como encontrar e corrigir transições CSS

Para encontrar todos os layout shifts causados por transições CSS, primeiro liste todas as transições da página e veja quais animam propriedades que afetam o layout. Depois, desative ou altere essas regras e meça se o CLS desaparece.

Dica de Core Web Vitals: esses shifts acontecem no início do carregamento da página e não em todas as visitas. Emule um celular, aplique throttling na rede e desative o cache. Em um carregamento lento, o navegador renderiza a página antes que o stylesheet ou script atrasado chegue, fazendo o shift aparecer com muito mais frequência.

Passo 1: encontre as transições CSS

Dá para encontrar transições CSS na mão: abra cada stylesheet e busque pela palavra 'transition'. Talvez leve dez minutos. Mais rápido: cole este snippet no console e aperte Enter.

(() => {

  let nodeTable = [];
  let nodeArray = [];

  // Get the name of the node
  function getName(node) {
    const name = node.nodeName;
    return node.nodeType === 1
      ? name.toLowerCase()
      : name.toUpperCase().replace(/^#/, '');
  }

  // Get the selector
  const getSelector = (node) => {
    let sel = '';

    try {
      while (node && node.nodeType !== 9) {
        const el = node;
        const part = el.id
          ? '#' + el.id
          : getName(el) +
          (el.classList &&
            el.classList.value &&
            el.classList.value.trim() &&
            el.classList.value.trim().length
            ? '.' + el.classList.value.trim().replace(/\s+/g, '.')
            : '');
        if (sel.length + part.length > (100) - 1) return sel || part;
        sel = sel ? part + '>' + sel : part;
        if (el.id) break;
        node = el.parentNode;
      }
    } catch (err) {
      // Do nothing...
    }
    return sel;
  };

  const getNodesWithTransition = (node) => {

    // Get the computed style
    let cs = window.getComputedStyle(node);
    let tp = cs['transition-property'];
    let td = cs['transition-duration'];

    // If there is a transition, add it to the table
    if (tp !== '' && tp !== 'none' && td != '0s') {
      nodeTable.push({ selector: getSelector(node), transition: cs['transition'] });
      nodeArray.push(node);
    }

    // Recursively call this function for each child node
    for (let i = 0; i < node.children.length; i++) {
      getNodesWithTransition(node.children[i]);
    }
  }

  // find all transitions
  getNodesWithTransition(document.body);

  // Display the results in the console
  console.log('%cReadable table of selectors and their transitions', 'color: red; font-weight: bold;');
  console.table(nodeTable);

  console.log('%cNodeList for you to inspect (harder to read but more info)', 'color: red; font-weight: bold;');
  console.log(nodeArray);


  // styles to temporarity override the transitions
  let selectors = nodeTable.map((item) => item.selector).join(', ');

  console.log('%cSpecific CSS to disable all transitions on this page', 'color: red; font-weight: bold;');
  console.log(`<style>${selectors}{transition-property: none !important;}</style>`);

  console.log('%cGlobal CSS to disable all transitions on this page (not suggested on production)', 'color: red; font-weight: bold;');
  console.log(`<style>*{transition-property: none !important;}</style>`);

})()

Ele exibe uma tabela com todas as transições da página e o elemento onde cada uma está.

cls snippet table

Agora analise a coluna de transições. Comece pelas linhas com all: ela cobre todas as propriedades animáveis, inclusive width e height. Depois, procure linhas que citem width ou height diretamente. Margin e padding também movem conteúdo — assim como top e left.

Passo 2: modifique as transições CSS

O snippet também gera o CSS para desativar essas transições. Para um teste rápido, vá pelo caminho mais fácil e desative tudo com uma única linha:

<style>*{transition-property: none !important;}</style>

Em produção, você precisa de mais critério. Remova apenas as propriedades de transição que não precisa, seletor por seletor. Por exemplo, mude #button{transition: all .2s} para #button{transition: background-color .2s}.

Passo 3: meça a mudança no layout shift

Último passo: medir. A minha extensão do Chrome Core Web Vitals Visualizer mostra os shifts na sua tela, e uma ferramenta de RUM como o CoreDash mostra se eles também sumiram para os visitantes reais. No CoreDash, você pode detalhar o CLS pelo elemento que sofreu o shift. Depois da alteração, aquele menu ou banner deve sair da lista, e o p75 no celular deve acompanhar.

layout shift transition measured by coredash

Boas práticas para transições

  1. Declare a propriedade exata. Escreva transition: background-color .2s ease em vez de transition: all — assim, uma propriedade que você não pretendia animar não move nada.
  2. Anime transform e opacity. O navegador pode rodar ambos no compositor e nenhum dos dois aciona layout. Use transform: translate() para mover um elemento e transform: scale() para redimensionar. Opacity cria o efeito de fade. É isso que o Google também recomenda.
  3. Se um elemento precisa de transição, defina width e height explícitos, ou um aspect ratio, tanto antes quanto depois do efeito. Assim, o conteúdo ao redor fica no lugar. Veja mais em como corrigir layout shifts causados por imagens com dimensionamento automático.
  4. Tenha cautela com o will-change. Ele avisa o navegador para se preparar para uma animação promovendo o elemento à sua própria camada no compositor. Isso tem um preço. Com will-change: transform, o elemento ganha um novo stacking context e vira o containing block de descendentes posicionados — e cada camada extra consome memória da GPU. Ative a propriedade via JavaScript logo antes da animação começar e remova assim que ela terminar. Não a deixe fixa no stylesheet.

Muitos sites ainda erram nisso. O Web Almanac de 2025 encontrou animações fora do compositor em 40% das páginas no celular. O CLS é o Core Web Vital com a maior taxa de aprovação: 81% no celular. Se o seu site está nos 19% reprovados, transições CSS podem ser a causa que você ainda não conferiu.

About the author

Arjen Karel is a web performance consultant and the creator of CoreDash, a Real User Monitoring platform that tracks Core Web Vitals data across hundreds of sites. He also built the Core Web Vitals Visualizer Chrome extension. He has helped clients achieve passing Core Web Vitals scores on over 925,000 mobile URLs.

Seu score do Lighthouse não conta a história toda.

Seus usuários reais estão em Android com 4G. Analiso o que eles vivem de fato.

Analisar dados de campo
Layout shift causado por transições CSS Core Web Vitals Layout shift causado por transições CSS