Programação Por Equipe DevBras

Capa do artigo: position: sticky não gruda? Menu fixo no topo em CSS

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.

  • fixed tira o menu do fluxo. O conteúdo sobe e o primeiro parágrafo nasce atrás da barra, a menos que o body ganhe um padding-top igual à altura do menu, que precisa ser atualizado sempre que o menu muda.
  • sticky nã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 do body. 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-top usa 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 um scrollIntoView().
  • Fundo opaco e z-index modesto. Com fundo transparente, o texto atravessa a barra. Um z-index: 99999 passa 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.

Avalie este artigo

Referência: http://techandblogger.blogspot.com.br/2012/04/menu-de-topo-que-acompanha-pagina.html