position: sticky não gruda? Menu fixo no topo em CSS
Menu fixo no topo com position: sticky: o CSS completo, a diferença para fixed e um script de console que acha o overflow que impede o menu de grudar.
Um menu que acompanha a rolagem precisa de duas declarações: position: sticky e top: 0. Quando ele não gruda, a causa quase sempre está fora dele: um ancestral com overflow, um pai do tamanho do próprio menu ou a falta do top. Este post monta o menu, explica a diferença para fixed e traz um script de console que aponta qual dessas causas está quebrando a sua página, testado no Chrome 156.
Garantir que esse menu não esconda o foco do teclado nem o título de uma âncora é outro problema, com critério próprio da WCAG e um teste automatizado. Isso está em menu fixo e WCAG 2.4.11: teste o foco escondido.
Sticky ou fixed
A diferença está no fluxo da página. Segundo a referência de position no MDN, um elemento sticky é posicionado pelo fluxo normal e depois deslocado em relação ao ancestral de rolagem mais próximo. Ele continua ocupando o espaço dele e só gruda quando a rolagem o levaria para fora da tela.
fixedtira o menu do fluxo. O conteúdo sobe e o primeiro parágrafo nasce atrás da barra, a menos que obodyganhe umpadding-topigual à altura do menu, que precisa ser atualizado sempre que o menu muda.stickynão pede compensação no topo. Para o cabeçalho de um site comum, é a escolha mais simples.
Use fixed quando o elemento precisa ficar preso à janela independente de onde está no HTML, como um botão flutuante ou um aviso de cookies.
O menu completo
O exemplo é a documentação de um sistema interno com 40 seções e um índice no topo:
<body>
<a class="pular" href="#conteudo">Pular para o conteúdo</a>
<header class="topo">
<a href="/">Manual do Estoque</a>
<nav aria-label="Seções">
<a href="#requisitos">Requisitos</a>
<a href="#banco">Configurar o banco</a>
<a href="#deploy">Deploy</a>
</nav>
</header>
<main id="conteudo" tabindex="-1">
<h2 id="requisitos">Requisitos</h2>
...
</main>
</body>
:root { --altura-topo: 4rem; }
html { scroll-padding-top: calc(var(--altura-topo) + 0.75rem); }
.topo {
position: sticky;
top: 0;
z-index: 10;
min-height: var(--altura-topo);
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 0 1rem;
background: #ffffff; /* opaco: o texto não aparece por baixo */
border-bottom: 1px solid #d9d9d9;
}
.pular { position: absolute; left: 1rem; top: -10rem; }
.pular:focus { top: 1rem; z-index: 20; }
@media (prefers-reduced-motion: no-preference) {
html { scroll-behavior: smooth; }
}
@media (max-width: 40rem) {
:root { --altura-topo: 3rem; }
}
Cada peça tem um motivo:
- O
headeré filho direto dobody. O sticky só gruda dentro do bloco pai; como o pai é a página inteira, o menu acompanha a rolagem do começo ao fim. - A altura mora numa variável. O
scroll-padding-topusa a mesma variável, então quem aumentar o menu não precisa caçar números espalhados pelo CSS. O scroll-padding-top faz o navegador descontar essa faixa ao rolar até uma âncora ou umscrollIntoView(). - Fundo opaco e
z-indexmodesto. Com fundo transparente, o texto atravessa a barra. Umz-index: 99999passa por cima de modais e menus suspensos que deveriam ficar na frente. - Rolagem suave só para quem não pediu menos movimento, pela media query
prefers-reduced-motion.
Por que o sticky não gruda
Quando o menu rola junto com a página como se nada tivesse sido declarado, o navegador não está com defeito. Uma destas condições está valendo:
1. Um ancestral tem overflow
O sticky gruda no ancestral de rolagem mais próximo. Um overflow: hidden, auto ou scroll num div que envolve a página, colocado anos atrás para esconder uma barra de rolagem horizontal, vira esse ancestral. Como ele não rola, o menu não tem onde grudar.
A troca segura é overflow-x: clip. Ela corta o que vaza na horizontal sem criar contêiner de rolagem, como explica a referência de overflow no MDN. No teste com Chrome 156, a mesma página com overflow: hidden no wrapper deixou o menu em top = -800 depois de rolar 800 px (rolou junto). Com overflow-x: clip, o menu ficou em top = 0.
2. O pai tem a altura do menu
Se o header está dentro de um div class="cabecalho" que só contém ele, o sticky gruda por zero pixels e vai embora com o pai. Suba o header para filho do body ou aplique o sticky no próprio wrapper.
3. Falta o top
Sem top (ou bottom), o sticky se comporta como relative. É o caso mais fácil de não ver, porque nenhuma ferramenta reclama.
Um script que aponta a causa
Cole no console do DevTools, com o seletor do seu menu:
// Cole no console do DevTools; troque o seletor pelo do seu menu.
(function porQueNaoGruda(seletor) {
const el = document.querySelector(seletor);
const est = getComputedStyle(el);
const avisos = [];
if (est.position !== 'sticky') avisos.push(`position é ${est.position}, não sticky`);
if (['top', 'bottom', 'left', 'right'].every((l) => est[l] === 'auto')) avisos.push('falta top (ou bottom)');
for (let a = el.parentElement; a && a !== document.documentElement; a = a.parentElement) {
const o = getComputedStyle(a);
if (['hidden', 'auto', 'scroll'].some((v) => [o.overflow, o.overflowX, o.overflowY].includes(v))) {
avisos.push(`ancestral com overflow ${o.overflowX}/${o.overflowY}: ${a.tagName.toLowerCase()}${a.className ? '.' + a.className : ''}`);
}
}
const pai = el.parentElement.getBoundingClientRect().height;
if (pai - el.getBoundingClientRect().height < 50) avisos.push(`o pai tem só ${Math.round(pai)} px: o menu não tem onde grudar`);
console.log(avisos.length ? avisos.join('\n') : 'Nada óbvio: confira se a rolagem é da janela ou de um contêiner');
return avisos;
})('.topo');
Na página de teste com o wrapper problemático, a saída foi:
ancestral com overflow hidden/hidden: div.pagina
Se o script não acusa nada e mesmo assim o menu não gruda, confira se quem rola é a janela. Layouts com height: 100vh e overflow: auto no main fazem o main rolar, e um menu fora dele não tem motivo para grudar.
No celular, menos barra
Uma barra de 64 px numa tela com 640 px de altura útil ocupa 10% da leitura, e mais com o teclado virtual aberto. Em telas estreitas, deixe fixos só a marca e o botão que abre o menu, e mande os links para um painel que abre e fecha direito, como em menu retrátil que fecha. A variável menor no @media já ajusta o scroll-padding-top.
O menu que some ao rolar para baixo e volta ao rolar para cima muda de altura no meio da leitura e é justamente a animação que prefers-reduced-motion pede para evitar. Se o projeto exigir, desligue o efeito dentro de prefers-reduced-motion: reduce.
A mesma técnica serve para o cabeçalho de uma tabela longa, com o cuidado extra do contêiner de rolagem horizontal, explicado em cabeçalho fixo de tabela.
Referência: http://techandblogger.blogspot.com.br/2012/04/menu-de-topo-que-acompanha-pagina.html