overflow no CSS: hidden, auto, scroll e clip
O que fazer com o conteúdo que não cabe na caixa: rolagem, corte, reticências no texto e a barra que faz o layout inteiro pular para o lado.
overflow decide o que o navegador faz com o conteúdo que não cabe na caixa:
deixar vazar por cima do resto (visible), cortar (hidden e clip) ou
transformar a caixa numa área rolável (scroll e auto). A escolha não é
estética: ela muda a largura útil do conteúdo, cria um contexto de formatação
novo e pode matar um position: sticky três níveis abaixo.
Os exemplos são todos do catálogo on-line da Biblioteca Aurora: ficha de livro,
prateleira de capas, painel de filtros. Cada número deste artigo saiu de uma
medição real, feita num Chromium 151 dirigido pelo Playwright numa janela de
900 × 700 pixels — os scripts estão no texto e você consegue repetir. Toda
página de teste começa com body { margin: 0; font: 16px/24px system-ui; }, e é
dessa linha que vêm os múltiplos de 24 que aparecem nas medições de altura.
visible, hidden, clip, scroll e auto lado a lado
A ficha do livro tem altura fixa e a sinopse é maior do que ela. Cinco fichas
iguais, cinco valores de overflow:
<div class="ficha" id="visivel">
<p>Dom Casmurro, de Machado de Assis. Bento Santiago revisita a infância
no Rio e a amizade com Capitu.</p>
</div>
<!-- e mais quatro fichas idênticas a esta, com os id
oculto, cortado, rolagem e auto -->body { margin: 0; font: 16px/24px system-ui; }
.ficha { width: 240px; height: 90px; border: 1px solid; margin: 24px; }
#visivel { overflow: visible; }
#oculto { overflow: hidden; }
#cortado { overflow: clip; }
#rolagem { overflow: scroll; }
#auto { overflow: auto; }Em vez de olhar para a tela, medimos. clientHeight é a altura visível por
dentro, scrollHeight é a altura de tudo que existe ali, e scrollTop diz se
a caixa aceitou ser rolada:
for (const id of ['visivel', 'oculto', 'cortado', 'rolagem', 'auto']) {
const caixa = document.getElementById(id);
caixa.scrollTop = 40; // tenta rolar 40px para baixo
console.log(
getComputedStyle(caixa).overflow.padEnd(8),
'clientHeight', caixa.clientHeight,
'| scrollHeight', caixa.scrollHeight,
'| scrollTop', caixa.scrollTop
);
}Três coisas saltam daí.
A primeira: hidden rola. Ele cortou a sinopse na tela, mas a caixa virou
uma área rolável de verdade — o scrollTop foi para 38 (que é 128 - 90, o
máximo possível). Quem faz isso não é só o seu JavaScript: o navegador também
rola sozinho quando o usuário dá Tab até um campo que está escondido lá
embaixo. O resultado é um layout que “pula” sem ninguém ter tocado no mouse.
A segunda: clip não rola. O scrollTop continuou 0. clip corta e
encerra o assunto; é o valor certo quando você só quer impedir que um enfeite
vaze. Ele é mais novo que os outros e existe justamente porque hidden
carregava esse efeito colateral.
A terceira: repare que scrollHeight muda entre 112 e 128 dependendo do valor.
Numa caixa que rola, a margem inferior do parágrafo entra na conta da área
rolável; numa caixa visible, não entra. Não é bug — é a diferença entre
“conteúdo que transborda” e “área que rola”.
| valor | corta? | cria área rolável? | mostra barra |
|---|---|---|---|
visible |
não, vaza por cima | não | nunca |
hidden |
sim | sim, só sem barra | nunca |
clip |
sim | não | nunca |
scroll |
sim | sim | sempre, mesmo sem precisar |
auto |
sim | sim | só quando o conteúdo passa |
Na prática do dia a dia: auto para painéis que às vezes têm muito conteúdo,
clip para cortar enfeite, e hidden só quando você quer mesmo a área rolável
sem barra. scroll serve para reservar a barra e evitar o layout pulando — o
que hoje se resolve melhor com scrollbar-gutter, daqui a duas seções. Nos dois
casos vale a mesma ressalva, e ela vem medida mais adiante: onde a barra é
sobreposta, ela não ocupa layout, não existe pulo para evitar, e nem scroll
nem scrollbar-gutter reservam coisa alguma.
overflow-x e overflow-y não são tão independentes quanto parecem
Você escreve overflow-x: hidden e deixa o eixo vertical quieto. O navegador
não deixa quieto. Quatro estantes, quatro combinações:
body { margin: 0; font: 16px/24px system-ui; }
/* quatro <div class="estante"> vazias, com os id a, b, c e d */
.estante { width: 300px; height: 120px; }
#a { overflow-x: hidden; overflow-y: visible; }
#b { overflow-x: hidden; overflow-y: clip; }
#c { overflow-x: scroll; overflow-y: visible; }
#d { overflow-x: clip; overflow-y: visible; }for (const id of ['a', 'b', 'c', 'd']) {
const { overflowX, overflowY } = getComputedStyle(document.getElementById(id));
console.log(`#${id} computado -> overflow-x: ${overflowX} | overflow-y: ${overflowY}`);
}O visible que você escreveu virou auto em #a e em #c. A regra da
especificação é essa: visible num eixo é incompatível com um valor de rolagem
no outro, e quem perde é o visible. A caixa inteira vira contêiner de
rolagem, nos dois eixos.
#b mostra o mesmo acontecendo com clip, que foi rebaixado a hidden. E
#d é a única linha em que o que você escreveu sobreviveu inteiro: clip é o
único valor que convive com visible no outro eixo.
A barra de rolagem come largura do conteúdo
Antes de medir, vale separar os três números que o DOM oferece. Eles quase sempre são confundidos, e é neles que a barra de rolagem aparece:
offsetWidth vai de borda a borda. clientWidth para antes da barra de
rolagem. A diferença entre os dois é exatamente o que a barra tirou do seu
conteúdo. Se isso ainda soa estranho, a
lição de box model no CSS mostra de onde saem essas
caixas.
O painel de resultados da biblioteca tem 545px e uma grade de capas que se ajusta sozinha:
body { margin: 0; font: 16px/24px system-ui; }
.painel { width: 545px; height: 260px; overflow-y: auto; }
.classica::-webkit-scrollbar { width: 15px; } /* barra que ocupa espaço */
.reserva { scrollbar-gutter: stable; }
.grade { display: grid; gap: 16px; grid-template-columns: repeat(auto-fill, minmax(170px, 1fr)); }
.capa { height: 120px; background: #ddd; }
/* três painéis, cada um com uma .grade de nove .capa dentro:
#padrao = <div class="painel">
#soGutter = <div class="painel reserva">
#classica = <div class="painel classica reserva"> */for (const id of ['padrao', 'soGutter', 'classica']) {
const painel = document.getElementById(id);
const grade = painel.firstElementChild;
const colunas = getComputedStyle(grade).gridTemplateColumns.split(' ').length;
console.log(
`#${id}`.padEnd(10),
`offsetWidth ${painel.offsetWidth}`,
`| clientWidth ${painel.clientWidth}`,
`| a barra levou ${painel.offsetWidth - painel.clientWidth}px`,
`| colunas da grade: ${colunas}`
);
}Na última linha, quinze pixels custaram uma coluna inteira. A grade pedia
minmax(170px, 1fr) com gap: 16px: em 545px cabiam três colunas
(3 × 170 + 2 × 16 = 542), em 530px cabem duas. Isso é o
auto-fill do Grid fazendo o trabalho dele com a largura
que sobrou — e a largura que sobrou depende da barra.
E as duas primeiras linhas explicam por que esses 15px não apareceram sozinhos.
scrollbar-gutter: stable e o layout que para de pular
O painel com barra sobreposta não muda de largura. O painel com barra clássica muda: com três resultados ele não rola e tem a largura cheia; com quarenta ele rola e encolhe 15px. Todo o conteúdo se reacomoda no meio da busca.
scrollbar-gutter: stable reserva o espaço da barra o tempo todo, exista
conteúdo para rolar ou não:
body { margin: 0; font: 16px/24px system-ui; }
.painel { width: 545px; height: 260px; overflow-y: auto; }
.classica::-webkit-scrollbar { width: 15px; }
.reserva { scrollbar-gutter: stable; }
/* quatro painéis .painel.classica; os dois "Reserva" ganham também .reserva.
#curto e #curtoReserva têm 3 resultados dentro; #longo e #longoReserva, 40. */const largura = (id) => document.getElementById(id).clientWidth;
console.log('gutter: auto 3 resultados:', largura('curto'), '| 40 resultados:', largura('longo'));
console.log('gutter: stable 3 resultados:', largura('curtoReserva'), '| 40 resultados:', largura('longoReserva'));As duas linhas estão planas, e por motivos diferentes. A de cima está plana
porque a barra deste Mac é sobreposta e nunca ocupa espaço. A de baixo está
plana por construção: stable promete 530px agora e daqui a quarenta
resultados. Com a barra clássica da seção anterior, a linha de cima leria
545 | 530 — é esse par que faz o conteúdo centralizado andar 7,5px para o lado
quando a página cresce.
A regra que eu sigo: scrollbar-gutter: stable no html de qualquer site com
páginas de altura variável, e no contêiner de qualquer lista que carrega mais
itens depois. Custa uma linha e elimina uma categoria inteira de bug visual.
Reticências: text-overflow, white-space e o line-clamp de várias linhas
text-overflow: ellipsis sozinho não faz nada, e essa é a pergunta que mais
aparece. Ele precisa de duas companhias: o texto não pode quebrar linha, e a
caixa precisa cortar. Quatro títulos, quatro receitas:
body { margin: 0; font: 16px/24px system-ui; }
/* quatro <p class="titulo"> com o mesmo texto de 54 caracteres:
"Grande Sertão: Veredas, edição comemorativa de 70 anos".
#a fica só com o que .titulo dá; b, c e d acrescentam uma receita cada. */
.titulo { width: 220px; overflow: hidden; text-overflow: ellipsis; }
#b { white-space: nowrap; }
#c { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
#d { display: block; line-clamp: 2; }for (const id of ['a', 'b', 'c', 'd']) {
const titulo = document.getElementById(id);
const cortado = titulo.scrollWidth > titulo.clientWidth
|| titulo.scrollHeight > titulo.clientHeight;
console.log(
`#${id}`,
'| linhas visíveis', titulo.clientHeight / 24,
'| linhas totais', titulo.scrollHeight / 24,
'| cortado?', cortado,
'| textContent', titulo.textContent.length, 'caracteres'
);
}#a tem overflow: hidden e text-overflow: ellipsis, e mesmo assim mostra o
título inteiro em três linhas: sem white-space: nowrap, o texto simplesmente
quebra e nunca chega a estourar. #b acrescenta o nowrap e aí, sim: uma
linha só, scrollWidth maior que clientWidth, reticências na tela.
#c é o corte de várias linhas, com as três declarações -webkit- que
precisam andar juntas: duas linhas visíveis de três. E #d é o aviso: a versão
padronizada, line-clamp: 2 sem o -webkit-box, ainda não funcionou neste
Chromium 151 — mostrou as três linhas. Continue escrevendo o trio antigo por
enquanto.
Repare na última coluna: textContent tem 54 caracteres nos quatro casos. O
corte é só visual. O texto continua inteiro no DOM, o leitor de tela lê
tudo, e um Ctrl+C copia tudo. Isso é bom para acessibilidade e ruim se você
achava que tinha resolvido o tamanho do dado no banco.
E é dali que sai o truque prático: scrollWidth > clientWidth é como você
descobre, em JavaScript, se um título foi cortado — e só então coloca um
title com o nome completo, em vez de espalhar tooltip em elemento que nem
precisava.
overflow cria um contexto de formatação — e mata o position: sticky
Qualquer valor de overflow diferente de visible faz a caixa virar um bloco
de formatação novo: um mundinho onde os floats ficam presos e as margens não
atravessam a borda. Isso é usado há vinte anos como truque, e continua valendo.
body { margin: 0; font: 16px/24px system-ui; }
.destaque { width: 400px; background: #eee; }
.capa { float: left; width: 90px; height: 130px; }
.selo { margin-top: 40px; }
#b, #d { overflow: hidden; }<section class="destaque" id="a"><div class="capa"></div></section>
<section class="destaque" id="b"><div class="capa"></div></section>
<section class="destaque" id="c"><p class="selo">Novidade</p></section>
<section class="destaque" id="d"><p class="selo">Novidade</p></section>for (const id of ['a', 'b', 'c', 'd']) {
const secao = document.getElementById(id);
const filho = secao.firstElementChild;
const distancia = filho.getBoundingClientRect().top - secao.getBoundingClientRect().top;
console.log(
`#${id} overflow: ${getComputedStyle(secao).overflow.padEnd(8)}`,
`| altura da seção: ${secao.offsetHeight}px`,
`| o filho começa ${distancia}px abaixo do topo dela`
);
}#a tem uma capa flutuante de 130px dentro e mede zero de altura: o float
não conta. Com overflow: hidden, #b passa a medir os 130px. #c e #d
mostram o outro lado: o margin-top: 40px do selo escapou da seção em #c (a
altura ficou em 24px, só a linha de texto), e ficou preso dentro dela em #d
(40 + 24 + 16 = 80px). É a margem que colapsa, o mesmo fenômeno de
quando o margin-top não funciona.
Só que esse mesmo poder tem um custo, e é o bug mais frequente da lista.
position: sticky se prende ao contêiner de rolagem mais próximo. Se um
ancestral virou contêiner de rolagem sem querer, o sticky passa a se prender a
ele — e se esse ancestral não rola, o elemento não gruda em lugar nenhum:
body { margin: 0; font: 16px/24px system-ui; }
.catalogo { display: grid; grid-template-columns: 220px 1fr; gap: 24px; }
.filtros { position: sticky; top: 0; height: 120px; }
.resultados { height: 2400px; }
#tapado { overflow-x: hidden; }<main class="catalogo" id="livre">
<aside class="filtros" id="f1">Filtros</aside>
<div class="resultados"></div>
</main>
<main class="catalogo" id="tapado">
<aside class="filtros" id="f2">Filtros</aside>
<div class="resultados"></div>
</main>const quadro = () => new Promise((ok) => requestAnimationFrame(ok));
const topo = (id) => Math.round(document.getElementById(id).getBoundingClientRect().top);
window.scrollTo(0, 400); // 400px dentro do primeiro catálogo
await quadro();
console.log('.catalogo sem overflow overflow-y computado:',
getComputedStyle(document.getElementById('livre')).overflowY,
'| topo do aside:', topo('f1') + 'px');
window.scrollTo(0, 2800); // 400px dentro do segundo catálogo
await quadro();
console.log('.catalogo overflow-x: hidden overflow-y computado:',
getComputedStyle(document.getElementById('tapado')).overflowY,
'| topo do aside:', topo('f2') + 'px');O painel de filtros do primeiro catálogo ficou colado no topo da janela. O do
segundo subiu 400px junto com a página, ou seja, saiu da tela. E a causa está no
próprio <main class="catalogo">, o pai imediato do aside: aquele
overflow-x: hidden virou overflow-y: auto, pela regra da segunda seção deste
artigo. Ninguém escreveu overflow-y em lugar nenhum, e mesmo assim o main
virou o contêiner de rolagem mais próximo do sticky — um contêiner que tem
altura de sobra e nunca rola.
O detalhe cruel é que o overflow-x: hidden costuma ter sido colocado ali para
apagar uma rolagem horizontal — e o preço veio dois meses depois, num sticky
que “parou de funcionar sozinho”. Vale reler
position no CSS com essa regra na mão.
scroll-behavior, overscroll-behavior e scroll-snap
Quando a caixa vira área rolável, três propriedades passam a valer para ela. A prateleira de lançamentos da biblioteca usa as três:
body { margin: 0; font: 16px/24px system-ui; }
/* três prateleiras iguais, com oito <article class="capa"> cada:
#livre, #encaixa e #suave */
.prateleira { display: flex; gap: 20px; width: 600px; overflow-x: auto; }
.capa { flex: 0 0 160px; height: 220px; }
#encaixa { scroll-snap-type: x mandatory; }
#encaixa .capa { scroll-snap-align: start; }
#suave { scroll-behavior: smooth; }const espera = (ms) => new Promise((ok) => setTimeout(ok, ms));
const livre = document.getElementById('livre');
const encaixa = document.getElementById('encaixa');
const suave = document.getElementById('suave');
console.log('prateleira: clientWidth', livre.clientWidth,
'| scrollWidth', livre.scrollWidth,
'| rolagem máxima', livre.scrollWidth - livre.clientWidth, 'px');
livre.scrollLeft = 130;
encaixa.scrollLeft = 130;
await espera(300);
console.log('scrollLeft = 130 -> sem snap:', livre.scrollLeft, '| com snap:', encaixa.scrollLeft);
encaixa.scrollLeft = 260;
await espera(300);
console.log('scrollLeft = 260 -> com snap:', encaixa.scrollLeft);
suave.scrollLeft = 540;
console.log('scroll-behavior: smooth -> no ato:', suave.scrollLeft);
await espera(600);
console.log('scroll-behavior: smooth -> 600ms depois:', suave.scrollLeft);Sem snap, scrollLeft = 130 fica em 130 e a capa aparece cortada pela metade.
Com scroll-snap-type: x mandatory, o navegador puxa para 180 — o começo da
segunda capa, porque as capas medem 160px com 20px de intervalo. E 260 também
virou 180: o snap escolhe o ponto mais próximo, para trás se for o caso.
A terceira dupla de linhas mostra o scroll-behavior: smooth: no instante
seguinte à atribuição, scrollLeft ainda era 0; a animação estava começando.
Seiscentos milissegundos depois, chegou nos 540. Guarde isso: com smooth,
ler scrollLeft logo depois de escrever nele devolve o valor antigo.
Falta a terceira. Quando você chega ao fim de uma lista rolável e continua
rolando, o navegador repassa a rolagem para a página atrás — é o scroll
chaining. overscroll-behavior: contain interrompe esse repasse:
/* a página precisa ter para onde rolar, senão não há encadeamento para observar */
body { margin: 0; font: 16px/24px system-ui; height: 3000px; }
/* duas listas de 800px de conteúdo: <div class="sugestoes" id="solto"> e id="preso" */
.sugestoes { width: 400px; height: 200px; overflow-y: auto; margin: 40px; }
#preso { overscroll-behavior: contain; }Este é o único teste do artigo que não roda dentro da página: scrollLeft no
JavaScript não encadeia rolagem nenhuma, então precisa de uma roda de mouse de
verdade. Quem dá a rolada é o Playwright, de fora, com a lista já no fim:
import { chromium } from 'playwright-core';
import { pathToFileURL } from 'node:url';
const navegador = await chromium.launch();
for (const [id, y] of [['solto', 140], ['preso', 380]]) {
const p = await navegador.newPage({ viewport: { width: 900, height: 600 } });
await p.goto(pathToFileURL('sugestoes.html').href);
await p.evaluate((alvo) => { document.getElementById(alvo).scrollTop = 9999; }, id);
await p.mouse.move(200, y); // ponteiro em cima da lista
await p.mouse.wheel(0, 400); // roda de verdade, 400px
await p.waitForTimeout(500);
const r = await p.evaluate((alvo) => ({
modo: getComputedStyle(document.getElementById(alvo)).overscrollBehaviorY,
lista: document.getElementById(alvo).scrollTop,
pagina: window.scrollY,
}), id);
console.log(`#${id} overscroll-behavior: ${r.modo.padEnd(8)} lista no fim (scrollTop ${r.lista}) -> a página rolou ${r.pagina}px`);
await p.close();
}
await navegador.close();As duas listas estavam no fim. Na primeira, os 400px da roda vazaram para a página. Na segunda, morreram na lista. É a linha que falta em todo menu de sugestão de busca, todo chat lateral e todo modal com conteúdo comprido.
Achando o elemento culpado pela rolagem horizontal
Agora o problema que ninguém consegue depurar no olho. A página do catálogo rola para o lado e não dá para ver por quê:
<style>
body { margin: 0; font: 16px/24px system-ui; }
.pagina { max-width: 960px; margin: 0 auto; padding: 0 24px; }
.faixa { width: 100vw; background: #f2e6c9; padding: 16px 0; }
.resultados { display: grid; gap: 16px; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); list-style: none; padding: 0; }
.cartao { border: 1px solid #ccc; padding: 12px; }
.codigo { font: 14px/20px ui-monospace, monospace; }
</style>
<main class="pagina">
<section class="faixa">Semana do Livro Infantil na Biblioteca Aurora</section>
<ul class="resultados">
<li class="cartao"><h3>Dom Casmurro</h3>
<p class="codigo">https://catalogo.bibliotecaaurora.org.br/emprestimos/9788535902778</p></li>
<li class="cartao"><h3>Vidas Secas</h3><p class="codigo">9788501012111</p></li>
<li class="cartao"><h3>Quarto de Despejo</h3><p class="codigo">9788503012577</p></li>
</ul>
</main>Em vez de comentar <div> por <div>, cole este script no console. Ele faz
duas varreduras: a primeira acha as caixas que têm conteúdo escondido por
dentro, a segunda acha quem literalmente passa da borda direita da janela.
const nome = (el) =>
el.tagName.toLowerCase() +
(el.id ? `#${el.id}` : '') +
(el.classList.length ? `.${[...el.classList].join('.')}` : '');
const janela = document.documentElement.clientWidth;
console.log(`janela ${janela}px | estouro horizontal da página: ${document.documentElement.scrollWidth - janela}px`);
for (const el of document.querySelectorAll('*')) {
if (el.scrollWidth > el.clientWidth) {
console.log(` rola por dentro: ${nome(el)} — clientWidth ${el.clientWidth}, scrollWidth ${el.scrollWidth}`);
}
}
for (const el of document.querySelectorAll('*')) {
const direita = Math.round(el.getBoundingClientRect().right);
if (direita > janela) {
console.log(` passa da borda: ${nome(el)} — termina em ${direita}px, ${direita - janela}px além da janela`);
}
}A lista de cima é a cadeia de contêineres — html, body e main.pagina
apenas repetem o mesmo estouro de 24px que veio de baixo. A resposta está na
segunda lista, e ela tem um item só: section.faixa, terminando em 924px numa
janela de 900. A causa é width: 100vw dentro de um pai com padding: 0 24px.
A faixa começa no pixel 24 e mede a janela inteira, então sobra pela direita
exatamente o valor do padding. Vale conferir a
lição de unidades no CSS: vw mede a janela, % mede
o pai — dentro de um pai com padding, as duas nunca coincidem.
E os li.cartao e p.codigo? Aparecem só na primeira lista porque a URL de
empréstimo é uma palavra sem espaço, larga demais para o cartão. Ela vaza 308px
para fora dele — 555 de scrollWidth contra 247 de clientWidth — e ainda
assim termina em 592px, dentro da janela de 900. É um defeito visual real, passa
por cima do cartão vizinho, e não é o que faz a página rolar. As duas listas são
separadas exatamente para você não confundir os dois.
A correção são duas linhas:
- .faixa { width: 100vw; background: #f2e6c9; padding: 16px 0; }
+ .faixa { width: 100%; background: #f2e6c9; padding: 16px 0; }
- .codigo { font: 14px/20px ui-monospace, monospace; }
+ .codigo { font: 14px/20px ui-monospace, monospace; overflow-wrap: anywhere; }As duas varreduras voltaram vazias.
Agora o contraexemplo, que é a razão de tudo isto existir. Em vez de corrigir,
alguém escreve body { overflow-x: hidden } e roda o mesmo script:
Idêntica, linha por linha. O scrollWidth continua 924. A faixa continua
terminando em 924px. Nada foi consertado: a barra sumiu da tela e o resto
continua exatamente onde estava.
Falta responder o que esse paliativo custa — e aqui a resposta mais repetida na
internet está errada. overflow-x: hidden no body não quebra o
position: sticky da página. Existe uma exceção na especificação: quando o
html está em visible, o navegador propaga o overflow do body para a
viewport, e o body fica valendo visible. Não nasce contêiner de rolagem
nenhum. Ponha a mesma linha um nível abaixo, num wrapper qualquer, e aí sim.
Duas páginas, diferentes por uma linha só:
<style>
body { margin: 0; font: 16px/24px system-ui; }
.pagina { max-width: 960px; margin: 0 auto; padding: 0 24px; }
.faixa { width: 100vw; background: #f2e6c9; padding: 16px 0; }
.barra { position: sticky; top: 0; height: 60px; background: #ddd; }
.resultados { height: 2400px; }
/* cada página tem SÓ UMA destas duas linhas; o data-caso do body diz qual */
body { overflow-x: hidden; } /* página A: o paliativo no body */
.pagina { overflow-x: hidden; } /* página B: o mesmo hidden no wrapper */
</style>
<body data-caso="body { overflow-x: hidden }">
<main class="pagina">
<section class="faixa">Semana do Livro Infantil na Biblioteca Aurora</section>
<div class="barra">Filtros</div>
<div class="resultados"></div>
</main>const quadro = () => new Promise((ok) => requestAnimationFrame(ok));
const eixos = (el) => `${getComputedStyle(el).overflowX} / ${getComputedStyle(el).overflowY}`;
window.scrollTo(0, 400);
await quadro();
console.log(`página com ${document.body.dataset.caso}`);
console.log(' body overflow-x / y:', eixos(document.body));
console.log(' main.pagina overflow-x / y:', eixos(document.querySelector('.pagina')));
console.log(' topo do sticky depois de rolar 400px:',
Math.round(document.querySelector('.barra').getBoundingClientRect().top) + 'px');Na página A o body computa hidden, mas quem recebe esse valor de fato é a
viewport — que já era a área rolável da página. Nada mudou de dono, e o sticky
ficou colado em 0px. Na página B o mesmo hidden está no main.pagina, que
virou contêiner de rolagem de verdade (repare no overflow-y: auto que ninguém
escreveu), e a barra de filtros foi parar 344px acima do topo da janela: sumiu
da tela.
O problema é que o estouro raramente está no body — está numa faixa lá dentro,
e é no wrapper dela que a pessoa acaba colando o overflow-x: hidden. É o
caminho mais curto entre “sumi com a barra” e “o sticky parou de funcionar
sozinho”.
O que vem depois
overflow é a última peça do jeito que a caixa se comporta. A próxima é o que
se desenha nela: fundo, cantos e sombra ficam em
background, border-radius e box-shadow.
Se você quiser o mapa inteiro antes de continuar, o
guia de CSS mostra em que ordem cada assunto entra e o que já
está publicado.
E deixe o caçador de rolagem horizontal salvo como snippet no DevTools. É o tipo de script que você usa uma vez por mês e economiza meia hora toda vez.
Perguntas frequentes
Qual a diferença prática entre overflow hidden e clip?
Por que meu position sticky parou de funcionar?
text-overflow ellipsis não coloca as reticências. O que falta?
overflow-x hidden resolve rolagem horizontal indesejada?
Como saber por JavaScript se um texto foi cortado?
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 via Playwright 1.62.1, e as saídas exibidas são as reais — como produzimos este conteúdo.
Fontes consultadas
- MDN — overflow — developer.mozilla.org
- MDN — scrollbar-gutter — developer.mozilla.org
- MDN — text-overflow — developer.mozilla.org
- W3C — CSS Overflow Module Level 3 — w3.org
- MDN — CSS scroll snap — developer.mozilla.org


