Stacking context: como o CSS empilha as camadas
O que cria um contexto de empilhamento, por que z-index 9999 pode perder para z-index 1 e como ler a árvore de camadas de uma página inteira.
Stacking context é o agrupamento que o navegador cria para decidir quem fica na
frente de quem. Dentro de um contexto, os z-index dos filhos se comparam entre
si; de fora, o contexto inteiro vira uma peça só, e nenhum número de dentro
atravessa a fronteira. É por isso que um z-index: 9999 perde, calado, para um
z-index: 1.
Todos os exemplos deste artigo são da mesma loja: a Livraria Beira-Mar, com
cabeçalho fixo, vitrine de livros, um balão de “últimas unidades”, um widget de
avaliações de terceiros e um modal de “avise-me quando chegar”. Cada saída
colada aqui saiu de uma execução real no Chromium 151, dirigido por Playwright,
usando document.elementFromPoint como juiz: quem o navegador devolve naquele
ponto é quem está pintado por cima.
A página não é um plano, é uma pilha de pilhas
Pense em folhas de acetato empilhadas. O navegador pinta uma folha de cada vez, de baixo para cima, e o que você vê é o resultado sobreposto. Um contexto de empilhamento é um maço dessas folhas grampeado: por dentro tem ordem própria, mas para o maço de cima ele é uma folha só, que entra na pilha inteira numa posição única.
O <html> cria o contexto raiz — toda página tem pelo menos um. A partir daí,
certos elementos criam maços novos. Veja o caso mais comum de todos: o balão de
estoque precisa aparecer por cima do cabeçalho fixo.
<style>
body { margin: 0; font: 16px system-ui; }
.cabecalho { position: fixed; top: 0; left: 0; right: 0; height: 56px;
background: #1b2a4a; color: #fff; z-index: 2; }
.catalogo { position: relative; z-index: 1; margin-top: 40px;
height: 300px; background: #eef2f7; }
.estoque { position: absolute; top: 0; left: 40px;
width: 220px; height: 60px; background: gold; z-index: 9999; }
</style>
<div class="cabecalho" id="cabecalho">Livraria Beira-Mar</div>
<div class="catalogo" id="catalogo">
<div class="estoque" id="estoque">Últimas 3 unidades</div>
</div>O .estoque mora dentro do .catalogo e pede z-index: 9999. O cabeçalho pede
2. Nove mil ganha de dois, certo? O teste pergunta ao navegador quem ele
pinta no ponto (150, 50), onde os dois se cruzam:
// base = a string com o HTML do bloco acima, inteiro, <style> junto
const casos = {
'.catalogo { z-index: 1 }': '', // como está no CSS acima
'.catalogo { z-index: auto }': '.catalogo { z-index: auto; }',
};
for (const [nome, css] of Object.entries(casos)) {
await page.setContent(`${base}<style>${css}</style>`);
const info = await page.evaluate(() => ({
topo: document.elementFromPoint(150, 50).id,
zCatalogo: getComputedStyle(document.getElementById('catalogo')).zIndex,
}));
console.log(
`${nome.padEnd(28)} z-index do .catalogo=${info.zCatalogo.padEnd(5)}` +
` quem pinta em (150,50): #${info.topo}`,
);
}Uma linha de diferença, e o resultado inverte. O 9999 do balão nunca chegou a
disputar com o 2 do cabeçalho: ele disputou apenas com os irmãos dele, dentro
do .catalogo. Quem competiu lá em cima foi o .catalogo inteiro, com o 1
dele, contra o cabeçalho com 2.
z-index só compete dentro do próprio contexto
A regra cabe em duas frases, e vale a pena decorar as duas.
Primeira: position diferente de static mais um z-index que não seja
auto cria um contexto de empilhamento — com uma exceção que eu meço daqui a
pouco, o item de flex e o item de grid, que obedecem ao z-index mesmo estáticos.
Segunda: dentro de um contexto, os filhos são ordenados pelo z-index deles;
fora, o que vale é o z-index do contexto, não o dos filhos.
Foi exatamente isso que a saída mostrou. Com z-index: 1, o .catalogo virou
um maço grampeado e afundou inteiro para a posição 1. Com z-index: auto, o
.catalogo deixou de ser contexto, o balão passou a competir direto no contexto
raiz e o 9999 finalmente valeu alguma coisa.
Isso também explica por que a cascata não ajuda aqui. Ganhar a briga de
especificidade no CSS só garante que a sua
declaração de z-index é a que vai valer — não garante que ela vai ser
comparada com a do vizinho. São dois campeonatos diferentes, e o de empilhamento
acontece depois.
As sete propriedades que criam um contexto sem avisar
position mais z-index é o caso que todo mundo conhece. O problema é a lista
das outras — propriedades que ninguém escreve pensando em camada e que criam
contexto do mesmo jeito.
Para medir, montei um teste com três elementos: uma .secao que recebe a
propriedade sob suspeita e não tem z-index; uma filha .estoque com
z-index: 9999; e uma irmã .promo com z-index: 1. Se a .secao criar
contexto, o 9999 fica preso e a .promo vence.
<style>
body { margin: 0; font: 16px system-ui; }
.palco { position: relative; z-index: 0; width: 500px; height: 300px; }
.secao { width: 400px; height: 200px; background: #eef2f7; }
.estoque { position: relative; z-index: 9999; top: 20px; left: 20px;
width: 260px; height: 80px; background: gold; }
.promo { position: absolute; z-index: 1; top: 40px; left: 60px;
width: 260px; height: 80px; background: #ffb3b3; }
</style>
<div class="palco">
<div class="secao" id="secao"><div class="estoque" id="estoque">Últimas 3 unidades</div></div>
<div class="promo" id="promo">Frete grátis acima de R$ 99</div>
</div>// base = a string com o HTML acima; cada candidata entra só na .secao
const candidatas = {
'nada (referência)': '',
'opacity: 0.99': 'opacity: 0.99;',
'transform: translateZ(0)': 'transform: translateZ(0);',
'will-change: top': 'will-change: top;',
// …e as outras dezesseis, uma por linha da saída
};
console.log('propriedade aplicada em .secao | topo em (100,60) | criou contexto?');
console.log('---------------------------------------|------------------|----------------');
for (const [nome, decl] of Object.entries(candidatas)) {
await page.setContent(`${base}<style>.secao { ${decl} }</style>`);
const topo = await page.evaluate(() => document.elementFromPoint(100, 60).id);
console.log(
`${nome.padEnd(38)} | ${('#' + topo).padEnd(16)} | ${topo === 'promo' ? 'SIM' : 'nao'}`,
);
}Vale reparar nas três últimas linhas tanto quanto nas de cima. overflow: hidden
não cria contexto — ele recorta, e recortar não é empilhar. position: relative sozinho também não. E position: fixed cria contexto mesmo sem
z-index nenhum, o que faz de todo cabeçalho fixo uma fronteira.
Rodei uma segunda rodada, com os casos de borda e mais três linhas de controle:
container-type, apesar de existir justamente para conter o elemento, não
criou contexto de empilhamento no Chromium 151 — usar container query não custa
uma camada nova. Já contain depende do valor: layout, paint, strict e
content criam; size e style não.
As três linhas de controle confirmam o resto por negação. opacity: 1 e
filter: none estão lá e não criam nada: o gatilho não é escrever a propriedade,
é o valor computado sair diferente do inicial. E position: relative com
z-index: 0 cria — guarde essa, porque ela volta quando eu falar da ordem de
pintura.
A exceção que não é propriedade nenhuma: o display do pai
Falta um criador de contexto que não está no CSS do elemento, e sim no do pai
dele. Em item de flex e item de grid, um z-index diferente de auto cria
contexto mesmo com position: static — é a regra do Flexbox e do Grid, não a do
posicionamento. Reaproveitei o mesmo base, mantive a .secao estática o tempo
todo e só mexi no display do .palco. A .promo subiu para 5000, abaixo do
9999 da filha e acima de qualquer número que a .secao carregue, para que
#promo no topo signifique exatamente uma coisa: o 9999 ficou preso.
console.log('display do .palco | z-index da .secao | topo em (100,60) | criou contexto?');
console.log('------------------|-------------------|------------------|----------------');
for (const display of ['block', 'flex', 'grid']) {
for (const zi of ['auto', '0', '2']) {
await page.setContent(
`${base}<style>.palco { display: ${display} } .secao { z-index: ${zi} }` +
` .promo { z-index: 5000 }</style>`,
);
const r = await page.evaluate(() => {
const s = getComputedStyle(document.getElementById('secao'));
return { z: s.zIndex, topo: document.elementFromPoint(100, 60).id };
});
console.log(
`${display.padEnd(17)} | ${r.z.padEnd(17)} | ${('#' + r.topo).padEnd(16)} | ${r.topo === 'promo' ? 'SIM' : 'nao'}`,
);
}
}Com o pai em block, o z-index da .secao é ignorado — ela é estática, e
z-index em elemento não posicionado não vale nada. Basta o pai virar flex ou
grid e o mesmo z-index: 0 passa a grampear o maço. Na prática isso quer dizer
que trocar um display: block por display: flex num contêiner qualquer pode
criar contextos em filhos que já tinham z-index escrito e ignorado até então.
É uma mudança de uma palavra que reorganiza camadas.
Se for para guardar sete nomes na cabeça, guarde estes — são os que aparecem em CSS escrito por gente que não estava pensando em z-index:
| propriedade | por que costuma estar ali | cria contexto |
|---|---|---|
opacity menor que 1 |
fade, estado desabilitado, hover suave | sim |
transform |
animação, centralização, hack de GPU | sim |
filter |
blur, grayscale, drop-shadow |
sim |
backdrop-filter |
vidro fosco atrás de modal | sim |
mix-blend-mode |
selo, marca-d’água, arte | sim |
isolation: isolate |
escrito de propósito, para isolar | sim |
will-change |
otimização preventiva | depende da propriedade citada |
opacity: 0.99 e o contexto criado por engano
opacity é a mais traiçoeira da lista, porque 0.99 é visualmente igual a 1.
Ninguém olha para a tela e desconfia. Fiz uma varredura de valores para achar
onde o navegador vira a chave:
for (const v of ['1', '0.999999', '0.99999999', '0.99', '0.5']) {
await page.setContent(`${base}<style>.secao { opacity: ${v}; }</style>`);
const info = await page.evaluate(() => ({
computado: getComputedStyle(document.getElementById('secao')).opacity,
topo: document.elementFromPoint(100, 60).id,
}));
console.log(
`opacity: ${v.padEnd(12)} computado: ${info.computado.padEnd(10)}` +
` topo: #${info.topo.padEnd(9)} contexto: ${info.topo === 'promo' ? 'SIM' : 'nao'}`,
);
}A linha do meio é a mais interessante do artigo. 0.999999 cria contexto;
0.99999999, com dois noves a mais, não cria — porque o Chromium arredonda
o valor computado para 1, e a regra olha para o valor computado, não para o
que você escreveu. O gatilho não é “é diferente de 1 no seu arquivo”, é “é menor
que 1 depois de computado”.
Pior: o contexto pode aparecer e sumir enquanto a pessoa usa a página. Um card
com transition: opacity e um :hover { opacity: .92 } cria contexto só
durante o hover. Montei o card .livro com o balão #balao (z-index: 9999)
dentro dele e a .promo (z-index: 1) como irmã do card, e medi com o mouse do
próprio navegador:
// pagina = o card .livro com transition: opacity e .livro:hover { opacity: .92 }
const ler = () => ({
opacidade: getComputedStyle(document.getElementById('livro')).opacity,
topo: document.elementFromPoint(100, 60).id,
});
await page.setContent(pagina);
console.log('antes do hover ->', await page.evaluate(ler));
await page.mouse.move(30, 150); // ponto dentro do .livro
await page.waitForTimeout(300);
console.log('durante o hover ->', await page.evaluate(ler));
await page.mouse.move(480, 290);
await page.waitForTimeout(300);
console.log('depois do hover ->', await page.evaluate(ler));O balão está por cima, some quando o mouse entra no card, e volta quando sai. É o tipo de bug que a pessoa não consegue reproduzir com o DevTools aberto, e que o time fecha como “não consegui reproduzir”.
transform, filter e will-change: os culpados silenciosos
transform empata com opacity em número de vítimas, mas por outro motivo: ele
quase nunca é escrito para transformar coisa alguma. transform: translate3d(0, 0, 0) e transform: translateZ(0) são hacks antigos para empurrar o elemento
para a GPU. Continuam em milhares de bases de código, em regras genéricas do
tipo .carrossel { transform: translate3d(0,0,0) }, e cada uma delas grampeia
um maço de folhas no meio do caminho.
O mesmo vale para animação de entrada. Se você anima um card com
transition e transform, esse card é um
contexto — durante a animação e depois dela, enquanto a propriedade final não
for none.
will-change é o caso mais sutil, e a saída explica melhor que qualquer
parágrafo: will-change: transform e will-change: opacity criaram contexto,
mas will-change: top não criou. A regra é composta — will-change só cria
contexto quando a propriedade citada seria capaz de criar por conta própria. Ele
avisa o navegador “prepare-se para animar isto”, e o navegador se prepara
promovendo o elemento a camada. Como top não cria contexto, avisar sobre top
também não cria.
A ordem de pintura dentro de um contexto, camada por camada
Até aqui só falamos de quem é contexto. Falta a outra metade: dentro de um contexto, o navegador pinta em sete camadas, nesta ordem, de baixo para cima.
- O fundo e as bordas do próprio elemento que criou o contexto.
- Os filhos com
z-indexnegativo, do mais negativo para o menos. - Os blocos no fluxo normal, não posicionados.
- Os elementos flutuantes.
- O conteúdo em linha — texto,
span,inline-block. - Os filhos posicionados com
z-index: autoouz-index: 0. - Os filhos com
z-indexpositivo, do menor para o maior.
Dá para verificar essa lista sem confiar em ninguém. Empilhei seis elementos
exatamente no mesmo retângulo, um de cada camada, e chamei
document.elementsFromPoint, que devolve os elementos do topo para o fundo:
<style>
body { margin: 0; font: 16px system-ui; }
.vitrine { position: relative; z-index: 0; width: 320px; height: 90px;
margin: 30px; background: #dfe9f3; }
.vitrine > * { width: 300px; height: 60px; }
.fundo-negativo { position: relative; z-index: -1; background: #f6c; }
.bloco-no-fluxo { margin-top: -60px; background: #cfc; }
.flutuante { float: left; margin-top: -60px; background: #fc9; }
.em-linha { display: inline-block; margin-top: -60px; background: #9cf; }
.posicionada { position: relative; margin-top: -60px; background: #ccf; }
.z-positivo { position: relative; z-index: 1; margin-top: -60px; background: #ff9; }
</style>
<div class="vitrine" id="vitrine">
<div class="fundo-negativo" id="1-z-negativo">z-index: -1</div>
<div class="bloco-no-fluxo" id="3-bloco-no-fluxo">bloco no fluxo</div>
<div class="flutuante" id="4-float">float</div>
<span class="em-linha" id="5-inline">inline-block</span>
<div class="posicionada" id="6-z-auto">position: relative (z-index: auto)</div>
<div class="z-positivo" id="7-z-positivo">z-index: 1</div>
</div>const retangulos = [...document.querySelectorAll('.vitrine > *')].map((el) => {
const r = el.getBoundingClientRect();
return `${el.id.padEnd(18)} x:${Math.round(r.left)}..${Math.round(r.right)}` +
` y:${Math.round(r.top)}..${Math.round(r.bottom)}`;
});
console.log('retângulos de cada camada:');
for (const linha of retangulos) console.log(' ' + linha);
const pilha = document
.elementsFromPoint(100, 60)
.map((el) => el.id || el.tagName.toLowerCase());
console.log('\nelementsFromPoint(100, 60), do topo para o fundo:');
pilha.forEach((id, i) => console.log(` ${i + 1}. ${id}`));Primeiro confirmei que os seis ocupam o mesmo retângulo, para o teste não medir geometria por engano:
E então a ordem que o navegador realmente pintou:
Bate com a lista, item por item, de trás para frente. E repare no detalhe da
penúltima posição: o filho com z-index: -1 ficou acima da .vitrine, que
é o elemento que criou o contexto. Isso é a camada 2 em ação — z-index
negativo afunda o filho até o fundo do maço, mas não além do fundo do maço. Se a
.vitrine não criasse contexto, esse mesmo filho afundaria até sumir atrás do
fundo do avô.
Duas consequências práticas dessa lista. Um texto simples numa <div> fica na
frente de um <div> de fundo que veio antes, sem ninguém escrever z-index —
é a camada 5 ganhando da 3. E um elemento posicionado com z-index: 0 não é
igual a um com z-index: auto para efeito de contexto: os dois pintam na camada
6, mas só o 0 cria contexto novo.
O diagrama: a árvore de contextos da Livraria Beira-Mar
Junte tudo numa página real e o desenho fica assim. Cada retângulo é um
contexto; o número ao lado é o z-index com que ele entra na disputa do
contexto pai.
A leitura é direta: a única disputa que existe no contexto raiz é entre 0,
100, 999 e 1000. Os dois números gigantes vivem em outro andar do prédio.
Mapeando os contextos reais de uma página com um script
Ler o CSS procurando as sete propriedades funciona numa página de exemplo. Numa página de verdade, com CSS de terceiros, é inviável. Melhor perguntar ao próprio navegador, depois de a página estar montada — o mesmo motivo pelo qual você inspeciona o DOM em vez de reler o HTML quando quer saber como o navegador montou a árvore.
O detector abaixo aplica as condições uma a uma sobre o valor computado:
const GATILHOS_WILL_CHANGE = new Set([
'opacity', 'transform', 'translate', 'rotate', 'scale', 'filter',
'backdrop-filter', 'perspective', 'clip-path', 'mask', 'mask-image',
'mix-blend-mode', 'isolation', 'contain', 'view-transition-name', 'z-index',
]);
const kebab = (p) => p.replace(/[A-Z]/g, (c) => '-' + c.toLowerCase());
function motivos(el) {
const s = getComputedStyle(el);
const pai = el.parentElement && getComputedStyle(el.parentElement);
const lista = [];
const z = s.zIndex === 'auto' ? '' : ` + z-index: ${s.zIndex}`;
if (el === document.documentElement) lista.push('elemento raiz <html>');
if (s.position === 'fixed' || s.position === 'sticky') lista.push(`position: ${s.position}${z}`);
else if (s.zIndex !== 'auto') {
if (s.position !== 'static') lista.push(`position: ${s.position}${z}`);
else if (pai && /flex|grid/.test(pai.display)) lista.push(`item de ${pai.display}${z}`);
}
if (parseFloat(s.opacity) < 1) lista.push(`opacity: ${s.opacity}`);
if (s.mixBlendMode !== 'normal') lista.push(`mix-blend-mode: ${s.mixBlendMode}`);
if (s.isolation === 'isolate') lista.push('isolation: isolate');
for (const prop of ['transform', 'rotate', 'scale', 'translate', 'filter',
'backdropFilter', 'perspective', 'clipPath', 'maskImage',
'offsetPath']) {
if (s[prop] && s[prop] !== 'none') lista.push(`${kebab(prop)}: ${s[prop]}`);
}
if (/\b(layout|paint|strict|content)\b/.test(s.contain)) lista.push(`contain: ${s.contain}`);
for (const p of s.willChange.split(',').map((x) => x.trim())) {
if (GATILHOS_WILL_CHANGE.has(p)) lista.push(`will-change: ${p}`);
}
return lista;
}E a varredura desce a árvore aumentando a indentação a cada contexto encontrado, para o resultado sair já no formato de árvore:
const achados = [];
(function visitar(el, profundidade) {
const lista = motivos(el);
if (lista.length) {
achados.push({ profundidade, alvo: el.id ? '#' + el.id : el.tagName.toLowerCase(), motivos: lista });
profundidade += 1;
}
for (const filho of el.children) visitar(filho, profundidade);
})(document.documentElement, 0);Rodando na página da livraria, com o CSS da casa e o CSS do widget de avaliações que veio de fora:
Dez contextos numa página com vinte linhas de markup. Repare no #rv-widget: o
CSS de terceiros trouxe um opacity: .99 que ninguém da equipe escreveu, e ele
prende tudo que está dentro. A nota 4,8 de 5 pede z-index: 2147483647, o
maior inteiro possível, e mesmo assim:
O overlay de vidro, com 999, cobre o elemento com o maior z-index que o CSS
aceita. Não é bug do navegador — é a #vitrine (transform) e o #rv-widget
(opacity) grampeando o maço duas vezes antes de a nota ter qualquer chance.
Como consertar sem sair aumentando o z-index
Com a árvore na mão, a correção deixa de ser tentativa e erro. São três caminhos, e a escolha depende de quem é dono do CSS problemático.
| situação | o que fazer | custo |
|---|---|---|
| o contexto no meio é seu e é acidental | remover a propriedade (opacity: 1, transform: none) |
zero, se ninguém dependia do efeito |
| o contexto no meio é necessário | subir o elemento na árvore, para fora dele | mexe no HTML e às vezes no componente |
| o CSS é de terceiros, você não pode tocar | tirar o elemento da árvore com a top layer | precisa de dialog ou popover |
A terceira linha é a novidade que resolve o caso mais desagradável. Um elemento
promovido à top layer — via <dialog> com showModal() ou via atributo
popover — é pintado fora de toda a árvore de contextos, acima de tudo, sem
z-index nenhum. Testei os dois balões lado a lado dentro do mesmo widget
grampeado:
<style>
body { margin: 0; font: 16px system-ui; }
.vitrine { transform: translate3d(0, 0, 0); }
.rv-widget { opacity: .99; }
.balao { position: absolute; top: 40px; left: 40px; z-index: 9999;
width: 240px; height: 60px; background: gold; }
#balao-popover { position: fixed; top: 40px; left: 320px; margin: 0; border: 0;
width: 240px; height: 60px; background: gold; }
.modal { position: fixed; top: 20px; left: 20px; width: 600px; height: 200px;
background: #bcd; z-index: 1000; }
</style>
<div class="vitrine">
<div class="rv-widget">
<div class="balao" id="balao">balão comum, z-index: 9999</div>
<div class="balao" id="balao-popover" popover>balão no popover, sem z-index</div>
</div>
</div>
<div class="modal" id="modal">modal, z-index: 1000</div>await page.evaluate(() => document.getElementById('balao-popover').showPopover());
const r = await page.evaluate(() => ({
comum: document.elementsFromPoint(100, 60).map((e) => e.id || e.tagName.toLowerCase()),
popover: document.elementsFromPoint(380, 60).map((e) => e.id || e.tagName.toLowerCase()),
}));
console.log('sobre o balão comum (100,60):', r.comum.join(' > '));
console.log('sobre o balão popover (380,60):', r.popover.join(' > '));
// controle: sem o transform e sem o opacity, o 9999 volta a valer
await page.addStyleTag({ content: '.vitrine { transform: none } .rv-widget { opacity: 1 }' });
const controle = await page.evaluate(() =>
document.elementsFromPoint(100, 60).map((e) => e.id || e.tagName.toLowerCase()),
);
console.log('controle, sem os dois (100,60):', controle.join(' > '));O balão comum, com 9999, fica atrás do modal. O balão com popover, sem
nenhum z-index, fica na frente. E a linha de controle fecha a prova: removendo
o transform da vitrine e o opacity do widget, o balão comum volta a vencer —
confirmando que o problema eram os dois contextos, e não o número.
O <dialog> aberto com showModal() faz o mesmo, e a medição deixa claro que
não é z-index disfarçado. Troquei o balão do popover por um <dialog> no
mesmo lugar da árvore, dentro dos dois contextos, e li o valor computado dele:
const pilhaEm = (x, y) =>
page.evaluate(
([x, y]) =>
document.elementsFromPoint(x, y).map((e) => e.id || e.tagName.toLowerCase()).join(' > '),
[x, y],
);
console.log('dialog fechado (380,60):', await pilhaEm(380, 60));
await page.evaluate(() => document.getElementById('balao-dialog').showModal());
console.log('dialog.showModal() (380,60):', await pilhaEm(380, 60));
console.log(
'z-index computado do dialog:',
await page.evaluate(() => getComputedStyle(document.getElementById('balao-dialog')).zIndex),
);Fechado, o dialog nem aparece na pilha. Aberto com showModal(), ele passa a
ser o primeiro elemento do ponto, à frente do modal de z-index: 1000 — com o
z-index computado em auto, ou seja, sem número nenhum. É esse o sentido de
“fora da árvore”: a top layer não disputa, ela é pintada depois de todo mundo.
Sobra um quarto caminho, que é preventivo em vez de corretivo: isolation: isolate. Ele cria um contexto de propósito na raiz de cada componente, para
que o z-index interno do componente seja um assunto local e nunca escape para
o resto da página. Custa uma camada e devolve previsibilidade — o oposto do
z-index: 9999 que só empurra o problema para o próximo componente.
O que vem depois
O caminho natural daqui é entender bem os valores de position, já que
fixed e sticky criam contexto sozinhos e relative não —
position no CSS trata cada um deles com medição. Se o
que trouxe você até aqui foi um elemento que não sobe, o roteiro de diagnóstico
está em z-index não funciona, com as cinco
causas separadas por sintoma.
E se você está montando a base de CSS por inteiro, o guia de CSS mostra a ordem de estudo, e a trilha de CSS traz as lições na sequência. Deixe o script de varredura salvo como snippet no DevTools: no dia em que algo não subir, ele responde em um segundo o que a leitura do CSS levaria uma tarde para achar.
Perguntas frequentes
Existe um z-index máximo no CSS?
z-index negativo funciona em qualquer elemento?
Como sei, no DevTools, se um elemento cria contexto?
Um contexto de empilhamento atrapalha a performance?
Dúvidas e comentários
Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.
Entrar para perguntarÉ o mesmo login gratuito dos cursos.
Nenhuma dúvida por aqui ainda — a primeira pode ser a sua.
Todo o código deste artigo foi executado em Chromium 151.0.7922.34 (Playwright 1.62.1, Node 24.16.0), e as saídas exibidas são as reais — como produzimos este conteúdo.
Fontes consultadas
- MDN — Stacking context — developer.mozilla.org
- CSS 2.2 — Appendix E: Elaborate description of Stacking Contexts — w3.org
- CSS Positioned Layout Level 3 — Painting order — w3.org
- MDN — will-change — developer.mozilla.org


