Pular para o conteúdo
Cursos

DevClub

LógicaFront-endBack-endMobile

IA Club

IA na prática
Estudar programaçãoEstudar IA
Erro resolvidoIntermediáriocódigo testado

z-index não funciona: 5 causas e a correção de cada

O elemento continua atrás mesmo com z-index alto: as cinco causas reais, como identificar cada uma no DevTools e a correção testada para todas.

Rodolfo Mori15 min de leitura

Se o z-index: 9999 não jogou o elemento para a frente, o número quase nunca é o problema. Ou o z-index não está sendo lido, porque o elemento é position: static, ou ele está sendo lido dentro de uma caixa fechada — um contexto de empilhamento criado por algum ancestral, que prende o filho lá dentro por mais alto que seja o valor.

São cinco causas, e elas se distinguem por dois testes de dez segundos. Este artigo reproduz cada uma num HTML mínimo e mostra a resposta do navegador.

Os exemplos vêm todos do mesmo site: a Cantina Bella Massa, uma pizzaria com delivery. O cabeçalho fixo, o menu de sabores que abre, a barra com o total do pedido, o botão flutuante do WhatsApp, a faixa de cookies e o modal de montar a pizza. São exatamente os elementos que brigam por camada em qualquer projeto real.

O teste que separa “não foi aplicado” de “foi aplicado e perdeu”

O erro parece um só, mas são dois estados bem diferentes, e cada um tem uma correção diferente. Duas perguntas resolvem a triagem.

Primeira: o navegador leu o valor? getComputedStyle responde. Se voltar auto, o problema é anterior ao empilhamento — a regra perdeu na cascata ou o valor é inválido.

Segunda: quem está por cima no ponto do conflito? document.elementFromPoint(x, y) devolve o elemento que receberia o clique naquela coordenada. É o juiz: ele não opina, ele responde quem venceu.

js
const botao = document.querySelector('.botao-whatsapp');
const estilo = getComputedStyle(botao);

console.log(estilo.position, estilo.zIndex);
console.log(document.elementFromPoint(450, 500));

Todas as saídas deste artigo vieram de um Chromium 151.0.7922.34 dirigido pelo Playwright 1.62.1, com viewport de 900×600. Cada caso é uma página mínima carregada por script; para ficar legível, o script imprime a tag e as classes do elemento que o elementFromPoint devolveu, em vez do nó inteiro. Quando o teste é de cor, o número sai de um screenshot de 1×1 pixel no ponto medido. As páginas de teste usam * { box-sizing: border-box } e body { margin: 0 }, e os blocos de CSS abaixo mostram só o que decide o empilhamento — cor e espaçamento ficaram de fora para não atrapalhar a leitura.

Causa 1: o elemento é static e o z-index nem chega a ser lido

O botão do WhatsApp fica no fluxo normal da página. A faixa de cookies é position: fixed, colada embaixo. As duas se sobrepõem, e o botão tem z-index: 9999.

html
<a class="botao-whatsapp" href="#">Pedir no WhatsApp</a>
<div class="faixa-cookies">Usamos cookies para lembrar seu endereço.</div>
css
.botao-whatsapp {
  position: static; /* o padrão — e depois relative, na segunda rodada */
  z-index: 9999;
  display: block;
  width: 240px;
  margin: 470px auto 0;
  padding: 16px;
  background: #16a34a;
  color: #fff;
}

.faixa-cookies {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  height: 120px;
  padding: 24px;
  background: #1f2937;
  color: #fff;
}

Rodei a mesma página duas vezes, mudando uma linha só: o position do botão.

position: static | getComputedStyle z-index: 9999 | elementFromPoint(450, 500): div.faixa-cookies position: relative | getComputedStyle z-index: 9999 | elementFromPoint(450, 500): a.botao-whatsapp

Repare na coluna do meio: o z-index computado é 9999 nos dois casos. O valor chegou, foi calculado e está guardado no elemento. Ele simplesmente não é usado na hora de pintar, porque z-index só entra na conta de elemento posicionado. É por isso que o painel Computed do DevTools engana: ele mostra 9999 com toda a confiança e o elemento continua atrás.

A correção é uma palavra: position: relative. Ela não move nada de lugar — o elemento fica onde estava, sem top nem left — e passa a valer o número que já estava escrito. Se você ainda não tem firmeza em qual valor de position usar, a lição de position no CSS trata cada um deles.

A exceção que ninguém lembra: item de flex e de grid

Filho direto de um container flex ou grid respeita z-index sem precisar de position. Coloquei o mesmo botão dentro de um .rodape, passei a margem para o pai e troquei só o display dele:

css
.rodape {
  display: flex; /* e depois grid, e depois block */
  margin-top: 470px;
}
.rodape display: block | filho position: static z-index: 9999 | por cima: div.faixa-cookies .rodape display: flex | filho position: static z-index: 9999 | por cima: a.botao-whatsapp .rodape display: grid | filho position: static z-index: 9999 | por cima: a.botao-whatsapp

O mesmo CSS no filho, três resultados diferentes por causa do pai. Guarde isso: quando o z-index funciona sem position e você não entende por quê, olhe o display do container.

Causa 2: o pai criou um contexto e prendeu o filho lá dentro

Agora o menu de sabores, que abre a partir do cabeçalho e precisa passar por cima da barra de total do pedido.

css
.cabecalho {
  position: relative;
  z-index: 1;
  height: 64px;
  background: #fff;
}

.menu-sabores {
  position: absolute;
  top: 64px;
  left: 24px;
  width: 260px;
  height: 220px;
  z-index: 9999;
  margin: 0;
  padding: 12px;
  list-style: none;
  background: #fef3c7;
}

.barra-total {
  position: relative;
  z-index: 2;
  margin-top: 120px;
  height: 72px;
  background: #111827;
}

O menu tem 9999. A barra tem 2. E a barra ganha:

.cabecalho z-index: 1 | .menu-sabores z-index: 9999 | elementFromPoint(150, 220): div.barra-total .cabecalho z-index: auto | .menu-sabores z-index: 9999 | elementFromPoint(150, 220): ul.menu-sabores .cabecalho z-index: 3 | .menu-sabores z-index: 9999 | elementFromPoint(150, 220): ul.menu-sabores

O que mudou entre as três linhas foi o z-index do cabeçalho, não o do menu. Quando um elemento é posicionado e tem z-index diferente de auto, ele cria um contexto de empilhamento: uma caixa fechada. Os z-index dos filhos passam a ser comparados só entre eles, ali dentro. Para o resto da página, o cabeçalho inteiro é um bloco só, e o número que representa esse bloco é o 1.

Pense num prédio: 9999 é o número do apartamento, e 1 é o andar. O apartamento 9999 do primeiro andar continua abaixo de qualquer coisa do segundo. O mecanismo completo do stacking context tem mais camadas que isso, mas essa imagem já resolve a maior parte dos casos.

Duas correções, e a escolha diz muito sobre o seu CSS:

  • tirar o z-index: 1 do cabeçalho — ele deixa de criar contexto e o menu volta a disputar de igual para igual (linha 2 da saída);
  • subir o cabeçalho para 3 — o contexto continua existindo e passa a ganhar como um todo (linha 3).

A segunda é quase sempre a certa. A primeira funciona hoje e volta a quebrar no dia em que alguém precisar do cabeçalho por cima de outra coisa.

Causa 3: opacity, transform ou filter num ancestral

Esta é a que faz gente desistir, porque o ancestral culpado não tem z-index nenhum — ele nem parece ter a ver com camada. Uma animação de fade que deixou opacity: 0.99, um transform: translateZ(0) posto para “melhorar a performance”, um filter de brilho no hover do card.

css
.lista-cardapio {
  position: relative;
  height: 300px;
  opacity: 0.99; /* sobrou de uma animação de entrada */
}

Para saber quais propriedades realmente fazem isso, rodei a mesma página 23 vezes, trocando só a declaração do ancestral. O menu tem z-index: 9999 e o concorrente tem 2: se o concorrente vencer, é porque o ancestral virou contexto e prendeu o menu.

(nenhuma) | contexto: nao | por cima: ul.menu-sabores opacity: 1 | contexto: nao | por cima: ul.menu-sabores opacity: 0.99 | contexto: SIM | por cima: div.barra-total transform: none | contexto: nao | por cima: ul.menu-sabores transform: translateX(0) | contexto: SIM | por cima: div.barra-total transform: translateZ(0) | contexto: SIM | por cima: div.barra-total filter: none | contexto: nao | por cima: ul.menu-sabores filter: blur(0px) | contexto: SIM | por cima: div.barra-total backdrop-filter: blur(2px) | contexto: SIM | por cima: div.barra-total will-change: transform | contexto: SIM | por cima: div.barra-total will-change: opacity | contexto: SIM | por cima: div.barra-total will-change: color | contexto: nao | por cima: ul.menu-sabores isolation: isolate | contexto: SIM | por cima: div.barra-total mix-blend-mode: multiply | contexto: SIM | por cima: div.barra-total contain: paint | contexto: SIM | por cima: div.barra-total contain: layout | contexto: SIM | por cima: div.barra-total perspective: 500px | contexto: SIM | por cima: div.barra-total mask: linear-gradient(#000, #000) | contexto: SIM | por cima: div.barra-total clip-path: inset(0) | contexto: SIM | por cima: div.barra-total overflow: hidden | contexto: nao | por cima: ul.menu-sabores content-visibility: auto | contexto: SIM | por cima: div.barra-total view-transition-name: cardapio | contexto: SIM | por cima: div.barra-total z-index: 0 | contexto: SIM | por cima: div.barra-total

Três leituras que valem mais que a lista:

  1. O valor neutro salva. opacity: 1, transform: none e filter: none não criam nada. opacity: 0.99 e filter: blur(0px) criam — e visualmente são idênticos aos neutros. Um blur de zero pixel muda o empilhamento da página inteira sem mudar um pixel na tela.
  2. transform: translateX(0) conta. Não é preciso mover nada. Qualquer valor diferente de none já basta, e é assim que a lição de transition e transform vira causa de bug de camada num hover.
  3. will-change depende do que você declara. will-change: transform cria contexto; will-change: color não. Ele promete ao navegador que aquela propriedade vai mudar, e o navegador se prepara — inclusive criando o contexto que a propriedade criaria.

E tem um caso que nem precisa de propriedade extra: o próprio position cria contexto sozinho em dois dos seus cinco valores.

css
.lista-cardapio {
  position: static; /* e depois relative, absolute, fixed e sticky */
  top: 0;
  left: 0;
  width: 900px;
  height: 300px;
}
.lista-cardapio position: static (sem z-index) | cria contexto: nao | por cima: ul.menu-sabores .lista-cardapio position: relative (sem z-index) | cria contexto: nao | por cima: ul.menu-sabores .lista-cardapio position: absolute (sem z-index) | cria contexto: nao | por cima: ul.menu-sabores .lista-cardapio position: fixed (sem z-index) | cria contexto: SIM | por cima: div.barra-total .lista-cardapio position: sticky (sem z-index) | cria contexto: SIM | por cima: div.barra-total

fixed e sticky criam contexto sozinhos, sem z-index nenhum. Como cabeçalho de site costuma ser um dos dois, esse é o ancestral culpado mais comum de todos.

Causa 4: os dois elementos não são irmãos no mesmo contexto

Aqui os dois lados estão presos, cada um no seu contexto, e você está comparando números que nunca se enfrentam. O modal de montar a pizza tem z-index: 100; o cabeçalho tem 50. O modal deveria ganhar. Só que o modal mora dentro de .conteudo (z-index: 1) e o cabeçalho mora dentro de .topo (z-index: 10).

html
<header class="topo"><nav class="cabecalho">Cantina Bella Massa</nav></header>
<main class="conteudo">
  <div class="modal-pedido">Monte sua pizza</div>
</main>
css
.topo      { position: fixed;    inset: 0 0 auto 0; height: 160px; z-index: 10; }
.cabecalho { position: relative; height: 160px;                    z-index: 50; }
.conteudo  { position: relative; padding: 200px 24px;              z-index: 1;  }
.modal-pedido { position: fixed; inset: 0;                         z-index: 100; }

Subi o z-index do modal quatro vezes, até o maior inteiro que o Chromium aceita:

.modal-pedido z-index: 100 | .conteudo z-index: 1 | por cima em (450, 80): nav.cabecalho .modal-pedido z-index: 1000 | .conteudo z-index: 1 | por cima em (450, 80): nav.cabecalho .modal-pedido z-index: 100000 | .conteudo z-index: 1 | por cima em (450, 80): nav.cabecalho .modal-pedido z-index: 2147483647 | .conteudo z-index: 1 | por cima em (450, 80): nav.cabecalho .modal-pedido z-index: 100 | .conteudo z-index: 20 | por cima em (450, 80): div.modal-pedido

2147483647 perdeu para 50. E a última linha resolve o caso mexendo num número que você nem estava olhando: .conteudo saiu de 1 para 20, e o modal com o humilde 100 original passou a ganhar.

O diagnóstico desta causa é esse teste: suba o número e veja se muda alguma coisa. Se você multiplicou por mil e o resultado é idêntico, pare de mexer nesse elemento. O número que decide está em outro lugar da árvore.

Causa 5: o valor está certo e o problema é a ordem de pintura

Sem z-index nenhum, dois elementos posicionados ainda se empilham — pela ordem no HTML. O card da pizza e o selo de promoção, os dois position: absolute, nenhum com z-index:

css
.card-pizza {
  position: absolute;
  top: 60px;
  left: 60px;
  width: 320px;
  height: 200px;
  background: #fff;
}

.selo-promo {
  position: absolute;
  top: 160px;
  left: 100px;
  width: 120px;
  height: 120px;
  background: #dc2626;
}

Quatro variações da mesma página, todas medidas no mesmo ponto de sobreposição, (150, 180). Entre uma e outra mudei a ordem dos dois no HTML, o position do card e o z-index do selo — nada mais. Na quarta o selo vira filho do card, e por isso o top e o left dele passam a 100px e 40px: é a conta que mantém o selo no mesmo lugar da tela agora que ele se posiciona a partir do card, e não mais do documento.

ambos absolute, sem z-index, HTML na ordem card -> selo : div.selo-promo ambos absolute, sem z-index, HTML na ordem selo -> card : div.card-pizza selo absolute e primeiro no HTML, card static : div.selo-promo selo com z-index: -1 dentro do card com fundo branco : div.card-pizza

Linha a linha:

  • 1 e 2 — trocar a ordem dos dois no HTML inverteu o resultado. Empatados no z-index, quem vem depois no documento pinta por cima. É por isso que mover uma div três linhas para baixo “conserta” o layout de um jeito que parece mágica.
  • 3 — o selo é posicionado e o card não. Mesmo aparecendo antes no HTML, o selo ganha: elemento posicionado pinta depois de elemento no fluxo normal, sempre, mesmo sem z-index.
  • 4z-index: -1 mandou o selo para trás do fundo do card. Ele não sumiu: foi pintado antes do background: #fff. Tire o fundo do card e ele reaparece no mesmo lugar.

A quarta linha tem uma inversão que quase toda explicação de z-index erra, e ela é fácil de medir. O selo só afunda porque o card não cria contexto de empilhamento — position: absolute com z-index: auto não cria. Sem contexto por perto, o negativo sobe até a raiz do documento e é pintado lá no fundo; o card, que é um elemento posicionado, é pintado bem depois, por cima dele. Basta dar um contexto ao card para o resultado virar do avesso:

css
.card-pizza { z-index: 0; } /* agora o card é o dono do contexto */

Com o selo em z-index: -1 nas três rodadas, medi a cor do pixel em (150, 180), onde os dois se cruzam. O selo é #dc2626 e o card é #fff, então a cor diz quem ficou na frente:

card sem z-index (nao cria contexto), com fundo | pixel em (150, 180): rgb(255,255,255) card sem z-index (nao cria contexto), sem fundo | pixel em (150, 180): rgb(220,38,38) card com z-index: 0 (cria contexto), com fundo | pixel em (150, 180): rgb(220,38,38)

Ou seja: o negativo só desaparece atrás do pai quando o pai não é dono de contexto nenhum. Dentro de um contexto, ele fica logo acima do fundo do dono.

O diagnóstico em três passos no DevTools

Passo 1 — o valor chegou? Selecione o elemento, vá em Computed e filtre por z-index. Se aparecer auto mesmo com a regra escrita, o problema é cascata ou valor inválido, e não empilhamento. Testei seis valores e li o computado de cada um:

css
/* seis rodadas separadas, uma declaração por vez —
   coladas no mesmo arquivo, só a última valeria */
.botao-carrinho { position: relative; z-index: 9999; }
.botao-carrinho { position: relative; z-index: 2147483647; }
.botao-carrinho { position: relative; z-index: 2147483648; }
.botao-carrinho { position: relative; z-index: 99999999999999; }
.botao-carrinho { position: relative; z-index: 9999.5; }
.botao-carrinho { position: relative; z-index: 9999px; }
z-index: 9999 -> getComputedStyle devolve: 9999 z-index: 2147483647 -> getComputedStyle devolve: 2147483647 z-index: 2147483648 -> getComputedStyle devolve: 2147483647 z-index: 99999999999999 -> getComputedStyle devolve: 2147483647 z-index: 9999.5 -> getComputedStyle devolve: auto z-index: 9999px -> getComputedStyle devolve: auto

z-index aceita inteiro e nada mais. Decimal ou com unidade é declaração inválida, o navegador descarta em silêncio e o valor volta para auto. Quando a regra é válida mas apareceu riscada no painel Styles, aí é a especificidade do seletor que perdeu.

Passo 2 — quem prende? Cole esta função no console e chame com o elemento teimoso. Ela sobe a árvore e diz qual ancestral cria contexto e por causa de quê:

js
function contextos(el) {
  const motivo = (n) => {
    if (n === document.documentElement) return 'raiz do documento';
    const s = getComputedStyle(n);
    if (s.position === 'fixed' || s.position === 'sticky') return 'position: ' + s.position;
    if (s.zIndex !== 'auto' && s.position !== 'static') return 'position: ' + s.position + ' + z-index: ' + s.zIndex;
    if (s.opacity !== '1') return 'opacity: ' + s.opacity;
    if (s.transform !== 'none') return 'transform';
    if (s.filter !== 'none') return 'filter';
    if (s.backdropFilter !== 'none') return 'backdrop-filter';
    if (s.mixBlendMode !== 'normal') return 'mix-blend-mode: ' + s.mixBlendMode;
    if (s.isolation === 'isolate') return 'isolation: isolate';
    if (s.willChange !== 'auto') return 'will-change: ' + s.willChange;
    if (s.contain.includes('paint') || s.contain.includes('layout')) return 'contain: ' + s.contain;
    if (s.perspective !== 'none') return 'perspective';
    if (s.clipPath !== 'none') return 'clip-path';
    if (s.maskImage !== 'none') return 'mask';
    return null;
  };

  const linhas = [];
  for (let n = el; n; n = n.parentElement) {
    const nome = n.tagName.toLowerCase() + (n.className ? '.' + n.className.trim().split(/\s+/).join('.') : '');
    const r = motivo(n);
    linhas.push([nome, r ? 'SIM' : '-', r || '']);
  }

  const larg = Math.max(...linhas.map((l) => l[0].length), 8);
  console.log('elemento'.padEnd(larg) + ' | cria contexto? | por causa de');
  for (const [nome, sim, r] of linhas) console.log(nome.padEnd(larg) + ' | ' + sim.padEnd(14) + ' | ' + r);
}

contextos(document.querySelector('.menu-sabores'));

Rodando numa página da Cantina em que o main tem z-index: 1 e um transform: translateZ(0) sobrou no meio do caminho:

elemento | cria contexto? | por causa de ul.menu-sabores | SIM | position: absolute + z-index: 9999 article.card-pizza | - | section.lista-cardapio | SIM | transform main.conteudo | SIM | position: relative + z-index: 1 body | - | html | SIM | raiz do documento

O primeiro SIM acima do seu elemento é a fronteira: dali para cima, o 9999 não vale mais nada. Aqui, section.lista-cardapio — e ninguém teria olhado para um transform.

Passo 3 — compare com o concorrente. Rode a mesma função no elemento que está ganhando. Onde as duas listas se encontram é o ancestral comum, e é lá que a disputa acontece de verdade. Os dois números que importam são os dos filhos diretos desse ancestral.

A correção que quase sempre resolve: tirar o elemento do lugar

Quando o culpado é um transform de um componente de terceiro, ou um sticky que você não pode remover, não adianta negociar com a árvore: tire o elemento dela. Historicamente isso é o portal — mover o modal para o fim do <body> com JavaScript, para que ele nasça fora de qualquer contexto.

Hoje o navegador tem uma solução melhor: a top layer, uma camada acima de todos os contextos de empilhamento da página. dialog.showModal() e a API de popover colocam o elemento lá, e z-index deixa de participar da conversa.

html
<div class="cabecalho-monstro">cabeçalho com z-index máximo</div>
<dialog class="modal-pedido">Monte sua pizza</dialog>
<div class="aviso-entrega" popover>Entregamos em 40 minutos</div>
css
.cabecalho-monstro {
  position: fixed;
  inset: 0;
  z-index: 2147483647;
}

.modal-pedido,
.aviso-entrega {
  width: 400px;
  height: 240px;
  background: #fff;
}
js
// três rodadas separadas: chamar show() e showModal() no mesmo dialog aberto
// lança InvalidStateError
document.querySelector('.modal-pedido').show();      // fluxo normal
document.querySelector('.modal-pedido').showModal(); // top layer
document.querySelector('.aviso-entrega').showPopover();

O .cabecalho-monstro é position: fixed com z-index: 2147483647, o teto do Chromium. O dialog e o popover não têm z-index nenhum:

dialog.show() (fluxo normal, sem z-index) : div.cabecalho-monstro dialog.showModal() (top layer, sem z-index) : dialog.modal-pedido showPopover() (top layer, sem z-index) : div.aviso-entrega

O mesmo dialog, o mesmo CSS, dois métodos diferentes. Com show() ele entra no fluxo e perde para o cabeçalho. Com showModal() ele vai para a top layer e ganha de um z-index de dois bilhões sem declarar nada.

Isso tem preço: showModal() vem com fundo escurecido, foco preso dentro do diálogo e fechamento pelo Esc. Para um modal é exatamente o que você quer. Para um menu suspenso, o certo é o atributo popover, que vai para a mesma camada sem prender o foco.

Por que z-index: 9999 é dívida e não solução

Toda vez que alguém escreve 9999, a próxima pessoa escreve 10000. Depois vem o 99999, e um dia alguém tenta 99999999999999 — que, como a saída do passo 1 mostrou, o Chromium recorta no teto e transforma em 2147483647. A escada acaba, e ela nunca respondeu à pergunta certa: qual camada é essa?

A saída é ter poucos valores, com nome, decididos uma vez. Esta é a escala que uso, com variáveis CSS num arquivo só:

css
:root {
  --camada-base: 0;
  --camada-cabecalho: 100;
  --camada-menu: 200;
  --camada-overlay: 300;
  --camada-modal: 400;
  --camada-aviso: 500;
}

.cabecalho { z-index: var(--camada-cabecalho); }
.menu-sabores { z-index: var(--camada-menu); }
camada valor quem mora aqui por que esse degrau
base 0 cards, seções, o conteúdo o padrão; quase nada precisa declarar
cabeçalho 100 cabeçalho fixo, barra de total tem que passar pelo conteúdo, e só
menu 200 menu de sabores, tooltip, autocomplete nasce do cabeçalho e precisa cobrir ele
overlay 300 fundo escuro do modal cobre a página inteira, menos o modal
modal 400 montar a pizza, confirmar pedido acima do próprio overlay
aviso 500 toast de “pedido enviado” aparece por cima até do modal

Os degraus de 100 existem para caber um caso não previsto no meio sem renumerar tudo. E a regra que sustenta a escala: cada componente fecha o próprio empilhamento com isolation: isolate. Com o componente isolado, os z-index de dentro dele não vazam para a página, e você pode usar 1, 2 e 3 à vontade lá dentro.

O que fazer agora

Abra o projeto onde o z-index está quebrado e rode os dois testes na ordem: getComputedStyle primeiro, para saber se o valor existe, e a função contextos() depois, para achar o ancestral que prende. Em cinco minutos você sabe qual das cinco causas é a sua — e cada uma tem uma correção diferente, nenhuma delas sendo aumentar o número.

Se o assunto ainda parece nebuloso, vale estudar o stacking context por dentro antes de voltar aqui. E se você está montando sua base de CSS agora, o guia completo de CSS mostra em que ordem cada peça entra.

  • css
  • z-index
  • erro
  • stacking context
  • position

Perguntas frequentes

O meu menu some quando o pai tem overflow hidden. É o mesmo problema?
Não. Medindo, overflow hidden não cria contexto de empilhamento — o menu não perdeu a disputa de camada, ele foi recortado pela caixa do pai. O sintoma parece igual, mas a correção é outra: tirar o elemento de dentro da caixa que recorta, ou renderizar ele na top layer.
O z-index aparece riscado no painel Styles. É a mesma causa?
Não. Riscado quer dizer que a declaração perdeu na cascata para outra regra, ou que o valor é inválido. É um problema anterior ao empilhamento: nesse caso o valor nem chega a existir no Computed. Confira o Computed antes de investigar contexto.
Para que serve isolation isolate, então?
Para o lado contrário do problema. Ele cria um contexto de propósito, para que os z-index de dentro de um componente não briguem com o resto da página. É a forma de dizer "os números daqui só valem aqui" sem depender de position nem de opacity.
z-index negativo tem algum uso legítimo?
Tem: mandar um enfeite para trás do conteúdo dentro do mesmo elemento, como uma faixa decorativa atrás de um texto. O cuidado é com o pai. Se o pai não cria contexto de empilhamento e tem background, o enfeite some atrás desse fundo. Medindo, um isolation isolate no pai resolve: com o contexto criado, o negativo passa a ser pintado logo depois do fundo dele.
Por que o mesmo layout só quebra no celular?
Quase sempre porque alguma media query liga, só naquela largura, uma propriedade que cria contexto — um position sticky no cabeçalho, um transform numa animação de menu. O contexto passa a existir apenas nesse intervalo, e junto com ele o bug.

Dúvidas e comentários

Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.

Todo o código deste artigo foi executado em Chromium 151.0.7922.34 via Playwright 1.62.1, e as saídas exibidas são as reais — como produzimos este conteúdo.

Fontes consultadas

  1. MDN — z-index — developer.mozilla.org
  2. MDN — Stacking context — developer.mozilla.org
  3. CSS 2.2 — Elaborate description of Stacking Contexts (Apêndice E) — w3.org
  4. MDN — Top layer — developer.mozilla.org

Continue por aqui