Layout shift causado por transições CSS
Saiba como encontrar e remover transições CSS que criam layout shifts
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
Table of Contents!
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á.

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

Boas práticas para transições
- Declare a propriedade exata. Escreva
transition: background-color .2s easeem vez detransition: all— assim, uma propriedade que você não pretendia animar não move nada. - Anime transform e opacity. O navegador pode rodar ambos no compositor e nenhum dos dois aciona layout. Use
transform: translate()para mover um elemento etransform: scale()para redimensionar. Opacity cria o efeito de fade. É isso que o Google também recomenda. - 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.
- 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. Comwill-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.
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