Programação Por Equipe DevBras

Capa do artigo: Cabeçalho fixo na tabela com position: sticky, e por que ele às vezes não gruda

Cabeçalho fixo na tabela com position: sticky, e por que ele às vezes não gruda

Mantenha o cabeçalho de uma tabela longa visível com CSS puro: sticky nas células, o contêiner certo rolando, fundo, borda que não some e a primeira coluna fixa.

O relatório de estoque tem 400 linhas e nove colunas: código, descrição, depósito, saldo, reservado, disponível, custo, preço e giro. Na linha 60, ninguém lembra mais se a quinta coluna é "reservado" ou "disponível", e a pessoa rola para cima e para baixo até errar o pedido de compra. A resposta curta: aplique position: sticky e top: 0 nas células th do cabeçalho, dê a elas um fundo opaco e garanta que exista só um elemento rolando, de preferência um contêiner com altura máxima e overflow: auto. Se não grudar, quase sempre há um ancestral com overflow que você não sabia que existia.

Não é preciso biblioteca nem duas tabelas empilhadas. Os navegadores atuais suportam sticky em células de tabela, e uma tabela só mantém as colunas alinhadas sem cálculo de largura em JavaScript.

Como o sticky decide onde grudar

Um elemento com position: sticky se comporta como normal até que, durante a rolagem, ele alcança a distância definida em top. A partir daí, fica preso naquela posição enquanto o seu bloco pai ainda estiver visível. Para a célula de cabeçalho, o bloco pai é a própria tabela: o cabeçalho acompanha a rolagem até a tabela acabar.

A pergunta importante é "preso em relação a quê". A resposta é: ao ancestral mais próximo que rola, ou seja, que tem overflow diferente de visible. Se nenhum ancestral rola, a referência é a janela. Isso explica os dois cenários que confundem.

  • Você quer que grude no topo da janela, mas algum ancestral tem overflow-x: auto (para rolagem horizontal no celular) ou overflow: hidden (para conter um float). O sticky passa a grudar dentro desse ancestral, que não rola na vertical, e na prática não gruda nunca.
  • Você quer que grude no topo de uma caixa com rolagem própria, mas a caixa não tem altura máxima. Ela cresce até caber a tabela inteira, quem rola é a página, e o cabeçalho rola junto.

O componente de tabela responsiva de vários frameworks CSS envolve a tabela num contêiner com overflow-x: auto. Quando o overflow de um eixo não é visible, o do outro eixo também deixa de ser, e esse contêiner vira a referência do sticky. É a causa mais comum de "funcionou no exemplo e não no meu projeto".

Como fazer o cabeçalho fixo passo a passo

  1. Use uma tabela de verdade, com thead, th com scope="col" e uma legenda.
  2. Envolva a tabela num contêiner com max-height e overflow: auto. Esse é o único elemento que rola.
  3. Aplique sticky nas células th do thead, com top: 0, fundo opaco e z-index.
  4. Troque border-collapse: collapse por separate com espaçamento zero, para a borda do cabeçalho não ficar para trás.
  5. Se precisar, fixe a primeira coluna com left: 0, e dê à célula do canto o maior z-index.
  6. Torne o contêiner acessível por teclado, com tabindex="0", papel de região e nome.
  7. Teste no celular: rolagem horizontal e vertical no mesmo contêiner.

O exemplo do relatório de estoque

O HTML

<div class="tabela-rolavel" tabindex="0" role="region" aria-labelledby="titulo-estoque">
  <table>
    <caption id="titulo-estoque">Saldo de estoque por depósito</caption>
    <thead>
      <tr>
        <th scope="col">Código</th>
        <th scope="col">Descrição</th>
        <th scope="col">Depósito</th>
        <th scope="col">Saldo</th>
        <th scope="col">Reservado</th>
        <th scope="col">Disponível</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <th scope="row">00012345</th>
        <td>Filtro de óleo FO-220</td>
        <td>Central</td>
        <td>148</td>
        <td>20</td>
        <td>128</td>
      </tr>
      <!-- demais linhas -->
    </tbody>
  </table>
</div>

A primeira célula de cada linha é um th scope="row", porque o código identifica a linha. Isso ajuda o leitor de tela a anunciar "00012345, Disponível, 128" e permite fixar essa coluna com o mesmo seletor.

O CSS

.tabela-rolavel {
  max-height: 70vh;
  overflow: auto;
}

.tabela-rolavel table {
  border-collapse: separate;
  border-spacing: 0;
  width: 100%;
}

.tabela-rolavel th,
.tabela-rolavel td {
  padding: 0.5rem 0.75rem;
  border-bottom: 1px solid #d0d5dd;
  text-align: left;
  white-space: nowrap;
}

.tabela-rolavel thead th {
  position: sticky;
  top: 0;
  z-index: 2;
  background: #f2f4f7;
  border-bottom: 2px solid #98a2b3;
}

.tabela-rolavel tbody th {
  position: sticky;
  left: 0;
  z-index: 1;
  background: #ffffff;
}

.tabela-rolavel thead th:first-child {
  left: 0;
  z-index: 3;
}

.tabela-rolavel:focus-visible {
  outline: 3px solid #1d4ed8;
  outline-offset: 2px;
}

O que cada regra resolve

O max-height: 70vh limita o contêiner a 70% da altura da janela; sem limite, ele cresceria e quem rolaria seria a página. O fundo do cabeçalho é obrigatório: sem ele, o cabeçalho fica transparente e as linhas passam por baixo, visíveis através do texto.

O border-collapse: separate com border-spacing: 0 parece igual ao collapse, mas com uma diferença decisiva. No modo collapse, as bordas pertencem à tabela, não à célula, e por isso não acompanham o cabeçalho grudado: a linha divisória rola embora e o cabeçalho fica sem borda. No modo separate, cada célula carrega a própria borda.

Os valores de z-index estabelecem quem fica por cima. A célula do canto, que é cabeçalho e primeira coluna ao mesmo tempo, gruda nos dois eixos e precisa ficar acima de todas. O cabeçalho fica acima da primeira coluna, que fica acima das células comuns.

Por fim, o tabindex="0" no contêiner permite que quem usa teclado chegue até ele com Tab e role com as setas. Sem isso, uma tabela que rola dentro de uma caixa fica inalcançável para quem não usa mouse.

Quando o site tem um menu fixo no topo

Se você prefere que a página inteira role e o cabeçalho grude na janela, remova o max-height e o overflow do contêiner e garanta que nenhum ancestral tenha overflow. Se o site tem uma barra fixa de 64 pixels no topo, use top: 64px nas células do cabeçalho, para elas grudarem logo abaixo da barra, e não por baixo dela. O mesmo cuidado aparece no guia sobre menu fixo que não cobre o título.

Como achar o ancestral que quebra o sticky

Quando o cabeçalho se recusa a grudar, cole isto no console do DevTools, na página do relatório:

let el = document.querySelector('thead th')
while ((el = el.parentElement)) {
  const s = getComputedStyle(el)
  if (s.overflowX !== 'visible' || s.overflowY !== 'visible') {
    console.log(el, s.overflowX, s.overflowY)
  }
}

O script sobe da primeira célula do cabeçalho até a raiz e lista todo ancestral cujo overflow não é visible. O primeiro da lista é a referência do sticky. Se ele não é o contêiner que você planejou, é ali que está o problema. No relatório de estoque, apareceu uma div de layout com overflow: hidden, colocada anos atrás para conter um float que nem existia mais.

No celular

Em tela estreita, as nove colunas não cabem. O mesmo contêiner rola nas duas direções: o cabeçalho gruda no topo, o código gruda à esquerda, e a pessoa nunca perde a referência de linha e coluna. Se a tabela é usada principalmente no celular, avalie as alternativas do guia sobre tabela larga no celular, como mostrar cada linha como uma ficha.

Erros comuns

  • Esquecer o fundo do cabeçalho. As linhas aparecem por trás do texto e o cabeçalho fica ilegível.
  • Manter border-collapse: collapse. A borda inferior do cabeçalho rola embora e ele parece flutuar sem separação.
  • Ancestral com overflow: hidden ou overflow-x: auto. O cabeçalho gruda numa caixa que não rola e, na prática, não gruda.
  • Contêiner sem altura máxima. Quem rola é a página e o cabeçalho rola junto.
  • Duas tabelas, uma só com o cabeçalho. As colunas desalinham com qualquer texto mais longo, e o leitor de tela perde a associação entre célula e cabeçalho.
  • Contêiner rolável sem tabindex. Quem usa teclado não consegue ver as linhas de baixo.

Checklist

  • Existe uma única tabela, com thead, th scope="col" e legenda.
  • Só um elemento rola: o contêiner com altura máxima, ou a página.
  • As células do cabeçalho têm position: sticky, top e fundo opaco.
  • A tabela usa border-collapse: separate com espaçamento zero.
  • A célula do canto tem o maior z-index quando a primeira coluna também é fixa.
  • O contêiner rolável recebe foco por teclado e tem nome.
  • Nenhum ancestral inesperado tem overflow diferente de visible.

Abra o maior relatório do seu sistema e role até a linha 100. Você ainda sabe, sem subir, qual é a quinta coluna?

Avalie este artigo

Referência: https://pt.stackoverflow.com/questions/271288/fixar-thead-ao-rolar-a-tabela