display no CSS: block, inline, inline-block e none
O que cada valor de display muda em largura, altura e quebra de linha, por que padding em inline não empurra nada e o que none faz que hidden não faz.
display responde duas perguntas ao mesmo tempo: como o elemento se comporta
por fora, na linha em que ele nasce, e como ele arruma os filhos por
dentro. É por isso que uma <div> empurra tudo para baixo e um <span> não,
mesmo os dois recebendo exatamente o mesmo CSS.
Os exemplos são todos da mesma página: a agenda da Clínica Veterinária Pata
Firme. Cada número desta lição saiu de uma medição real —
getBoundingClientRect() e getComputedStyle() rodando num Chromium
151.0.7922.34 sem interface, dirigido pelo Playwright 1.62.1. Nenhuma medida
aqui é estimativa.
A lição de seletores e cascata mostrou como
a regra chega até o elemento. display é a primeira decisão que essa regra toma
depois de chegar: que tipo de caixa esse elemento vai virar.
O fluxo normal: por que a div ocupa a linha inteira sem você pedir
Sem nenhum CSS de layout, o navegador coloca as caixas no que a especificação chama de fluxo normal: de cima para baixo, uma abaixo da outra, quando a caixa é de bloco; e da esquerda para a direita, dentro da linha de texto, quando a caixa é inline.
Estes quatro elementos têm o mesmo texto e o mesmo pai de 600px:
<section class="painel">
<div class="servico">Consulta</div>
<div class="servico">Vacina</div>
<span class="etiqueta">Consulta</span>
<span class="etiqueta">Vacina</span>
</section>O CSS não diz nada sobre tamanho — só pinta o fundo para dar de ver a caixa:
.painel { width: 600px; }
.servico { background: #eee; }
.etiqueta { background: #ddd; }Esta é a base usada em todas as medições da lição, e o .painel de 600px acima
continua valendo do começo ao fim. Vale anotar, porque os números decimais que
vão aparecer saem dela:
* { box-sizing: border-box; }
body {
margin: 0;
font-family: system-ui, sans-serif;
font-size: 18px;
line-height: 1.2;
}E este é o script de medição, que se repete o resto da lição inteira:
const medir = (rotulo, sel, i) => {
const b = document.querySelectorAll(sel)[i].getBoundingClientRect();
console.log(`${rotulo} -> largura ${b.width.toFixed(2)} | topo ${b.y}`);
};
console.log('largura do painel:', document.querySelector('.painel').getBoundingClientRect().width);
medir('div "Consulta"', '.servico', 0);
medir('div "Vacina" ', '.servico', 1);
medir('span "Consulta"', '.etiqueta', 0);
medir('span "Vacina" ', '.etiqueta', 1);Leia os quatro números de topo. As duas <div> estão em linhas diferentes (0 e
21,59 — a altura de uma linha, 18px × 1,2) e cada uma esticou até os 600px do
pai. Os dois <span> estão na mesma linha (43,1875 os dois) e cada um mediu
exatamente o próprio texto: 71,11px para “Consulta”, 53,23px para “Vacina”.
Ninguém escreveu display em lugar nenhum. <div> e <section> já nascem
display: block pela folha de estilo do navegador; <span>, <a> e <strong>
já nascem display: inline. display é a propriedade que troca esse padrão.
block aceita width, height e margem vertical
Numa caixa de bloco, as quatro medidas que você escreve valem. É o modo previsível, e é onde quase todo layout de página começa.
<section class="painel">
<div class="cartao" id="a">Consulta clínica</div>
<div class="cartao" id="b">Vacina antirrábica</div>
</section>.cartao {
width: 320px;
height: 90px;
margin: 24px 0;
background: #eee;
}const a = document.querySelector('#a').getBoundingClientRect();
const b = document.querySelector('#b').getBoundingClientRect();
console.log('cartão A: largura', a.width, '| altura', a.height);
console.log('cartão A vai de y =', a.top, 'até y =', a.bottom);
console.log('cartão B começa em y =', b.top);
console.log('espaço entre eles:', b.top - a.bottom);
console.log('altura do painel:', document.querySelector('.painel').getBoundingClientRect().height);width: 320px virou 320px de largura, height: 90px virou 90px de altura, e o
cartão parou de ocupar a linha inteira porque você deu uma largura menor que a
do pai. Isso é a caixa de bloco fazendo o que foi mandada.
Repare no espaço entre os dois cartões: 24px, não 48. Cada cartão pediu 24px embaixo e 24 em cima, e o navegador juntou os dois num só. Esse é o colapso de margem, um comportamento exclusivo das margens verticais no fluxo normal.
Ele acontece duas vezes nesta medição, e é a soma das duas que dá os 204px do
painel. A primeira é a que você acabou de ver, entre os dois irmãos: 24 e 24
viram 24, e o total sai dos 276px que a conta pediria (180 de altura mais 96 de
margem) para 252. A segunda é mais silenciosa — a margem de cima do primeiro
cartão e a de baixo do último escapam do painel, porque ele não tem borda
nem padding para segurar; é por isso que o painel começa em y = 24 e mede
90 + 24 + 90 = 204. O assunto tem lição própria em
margin-top não funciona; o que interessa
agora é que margin vertical existe aqui. Guarde isso para a próxima seção.
Quanto ao width: 320px virar 320px na tela, e não 320 mais padding e borda, o
responsável é o box-sizing — assunto do
box model, não do display.
inline: onde width, height e margem vertical viram enfeite
Esta é a seção que resolve a maior parte das dúvidas sobre display. É também o
erro mais comum de quem está começando: escrever height num <span> e não
entender por que nada acontece.
O aviso da clínica tem um selo no meio da frase:
<p class="aviso">A <span class="selo">antirrábica</span> é obrigatória.</p>
<p class="depois">Agende pelo telefone da recepção.</p>.aviso, .depois { margin: 0; }
.selo { padding: 30px; background: #ddd; }A margem dos dois parágrafos está zerada de propósito: assim o número que sobrar entre eles é obra do selo, e de mais nada.
30px de padding em volta de um texto de 21px deveria dar uma caixa de uns 81px
de altura, e o parágrafo deveria crescer junto. Metade disso acontece:
const rect = (s) => document.querySelector(s).getBoundingClientRect();
console.log('caixa pintada do selo:', rect('.selo').width.toFixed(2), 'x', rect('.selo').height.toFixed(2));
console.log('altura do parágrafo que contém o selo:', rect('.aviso').height.toFixed(2));
console.log('topo do parágrafo seguinte:', rect('.depois').top.toFixed(2));Aí está o defeito inteiro em três números. A caixa do selo tem 81px de altura — o padding existe e é pintado. Mas o parágrafo que contém o selo continua com 21,59px, e o parágrafo seguinte continua começando em 21,59.
Ou seja: o fundo cinza do selo vaza por cima e por baixo da linha, passa por trás do texto vizinho, e ninguém sai do lugar para acomodar. Numa caixa inline, o padding vertical é desenhado, mas não é contado na altura da linha.
Agora as três declarações que simplesmente não têm efeito. Cada linha da saída é uma medição feita depois de acrescentar mais um bloco de CSS ao mesmo selo:
/* 1º: só o fundo */ .selo { background: #ddd; }
/* 2º: + tamanho explícito */ .selo { width: 400px; height: 200px; }
/* 3º: + margem vertical */ .selo { margin: 40px 0; }
/* 4º: + margem nos 4 lados */ .selo { margin: 40px; }Quatro linhas, e só um número mudou no fim: a coluna esquerda, de 16,22 para
56,22 — os 40px de margem horizontal, que valem. width, height e a
margem vertical não moveram um pixel.
Por que a imagem e o botão parecem escapar da regra
Um detalhe que confunde: <img> computa display: inline e mesmo assim aceita
width e height. A explicação não está no display — está no fato de a
imagem ser um elemento substituído, uma caixa cujo conteúdo vem de fora do
documento e por isso tem dimensão própria.
Quatro elementos diferentes na mesma linha do formulário de peso, todos com o mesmo CSS:
<span class="rotulo">Peso</span>
<img class="icone" alt="" src="balanca.svg">
<input class="campo" value="12,4 kg">
<button class="acao">Salvar</button>.rotulo, .icone, .campo, .acao { width: 120px; height: 60px; }.rotulo é um <span>: ignorou as duas medidas. .icone é um <img>: inline e
obedeceu. .campo (<input>) e .acao (<button>) nem são inline — o
navegador já os entrega como inline-block, que é o próximo valor.
O erro que o navegador não avisa
O CSS não tem console de erro. Um valor com typo é descartado em silêncio, e o elemento fica com o valor anterior:
const rotulo = document.querySelector('.rotulo');
rotulo.style.display = 'inline-blok';
console.log('style.display = "inline-blok" -> computado:', getComputedStyle(rotulo).display);
rotulo.style.display = 'inline-block';
console.log('style.display = "inline-block" -> computado:', getComputedStyle(rotulo).display);
console.log('CSS.supports:', JSON.stringify({
'inline flex': CSS.supports('display', 'inline flex'),
'flow-root': CSS.supports('display', 'flow-root'),
contents: CSS.supports('display', 'contents'),
'inline-blok': CSS.supports('display', 'inline-blok'),
}));Nenhum aviso, nenhuma exceção: só o inline antigo continuando de pé. Quando um
display “não faz nada”, CSS.supports('display', 'o-valor') responde em um
segundo se o problema é o navegador ou o seu teclado.
inline-block: o meio-termo, e o vão de 4,56px que ninguém pediu
inline-block é literalmente o acordo entre os dois: por fora ele continua na
linha, ao lado do texto; por dentro ele vira uma caixa de bloco de verdade, com
largura, altura e margem funcionando.
Mesmo selo, mesmo padding de 30px, só trocando uma linha:
.selo { padding: 30px; background: #ddd; display: inline-block; }o vizinho de baixo desceu 60.00 px
O parágrafo saiu de 21,59 para 81,59px de altura, e o parágrafo seguinte desceu exatamente 60px — os dois paddings de 30. O padding parou de vazar e passou a ocupar espaço. É a mesma declaração de antes; o que mudou foi o tipo de caixa que a recebeu.
Agora o efeito colateral clássico. Três etiquetas de serviço, uma por linha no arquivo, do jeito que todo mundo escreve:
<div class="linha">
<span class="tag">Banho</span>
<span class="tag">Tosa</span>
<span class="tag">Vacina</span>
</div>.tag { display: inline-block; padding: 8px 16px; background: #ddd; }Existe um vão de 4,56px entre as etiquetas que ninguém escreveu no CSS. Ele não
é margem: é o espaço em branco do HTML. A quebra de linha entre os <span>
é um caractere de espaço, e como as caixas continuam na linha por fora, esse
espaço é renderizado igual a qualquer outro espaço de texto. 4,56px é a largura
de um espaço nesta fonte, neste tamanho.
Grudar as tags numa linha só resolve, mas ninguém quer escrever HTML assim. Duas saídas melhores:
/* gambiarra clássica: zera a fonte no pai e devolve nos filhos */
.linha { font-size: 0; }
.tag { font-size: 18px; }
/* solução de hoje: o pai deixa de montar linhas de texto */
.linha { display: flex; gap: 12px; }Com display: flex o vão fantasma some e o espaçamento passa a ser um número
que você escolheu, não um resíduo do arquivo. É o motivo de
Flexbox ter aposentado boa parte do uso de
inline-block em barras de botões e listas de etiquetas.
none, visibility: hidden e opacity: 0 não são sinônimos
As três “escondem”. Só que escondem coisas diferentes, e trocar uma pela outra
muda o layout, o clique e o leitor de tela. Este é o banner de feriado da
clínica, medido nas três versões. A mesma seção aparece três vezes na página,
mudando só o id:
<section class="painel bloco" id="none">
<div class="banner">Unidade fechada no feriado</div>
<p class="texto">Plantão 24h pelo WhatsApp.</p>
</section>
<!-- a mesma seção repetida com id="hidden" e com id="opacidade" -->.bloco { border: 1px solid #999; }
.banner { height: 60px; background: #ddd; }
.texto { margin: 0; }
#none .banner { display: none; }
#hidden .banner { visibility: hidden; }
#opacidade .banner { opacity: 0; }const rotulo = {
none: 'display: none',
hidden: 'visibility: hidden',
opacidade: 'opacity: 0',
};
for (const id of ['none', 'hidden', 'opacidade']) {
const secao = document.querySelector('#' + id);
const banner = secao.querySelector('.banner');
const b = banner.getBoundingClientRect();
const s = secao.getBoundingClientRect();
const alvo = document.elementFromPoint(s.left + 100, s.top + 30);
const pai = banner.offsetParent;
console.log(rotulo[id]);
console.log(` caixa do banner: ${b.width} x ${b.height}`);
console.log(` offsetParent: ${pai === null ? 'null' : pai.tagName.toLowerCase()}`);
console.log(` altura da seção: ${s.height.toFixed(2)}`);
console.log(` elementFromPoint no banner: .${alvo.className.split(' ')[0]}`);
console.log(` banner.innerText: ${JSON.stringify(banner.innerText)}`);
}Três diferenças que importam no dia a dia:
display: none |
visibility: hidden |
opacity: 0 |
|
|---|---|---|---|
| ocupa espaço | não (seção com 23,59px) | sim (83,59px) | sim (83,59px) |
| tem caixa | não (0 x 0, offsetParent nulo) |
sim | sim |
| recebe clique | não | não | sim |
| é lido pelo leitor de tela | não | não | sim |
O elementFromPoint da terceira linha é o detalhe que gera bug de produção: com
opacity: 0 o banner continua lá, invisível, roubando o clique de quem estiver
embaixo. É o “botão que não funciona” que você vai caçar por meia hora.
O innerText também separa os três, e de um jeito que pega gente no JavaScript:
ele devolve string vazia só no visibility: hidden, porque lê o que está
renderizado. No display: none não existe nada renderizado para ler, então
ele cai no texto cru do elemento e devolve a frase inteira.
A última linha da tabela veio de outra medição, na camada que quase ninguém
confere: a árvore de acessibilidade, que é o que o leitor de tela anuncia.
Quatro botões da agenda dentro de um <nav>, três deles escondidos de um jeito
diferente:
<nav class="painel">
<button class="none">Agendar banho</button>
<button class="hidden">Agendar tosa</button>
<button class="opacidade">Agendar vacina</button>
<button>Falar com a recepção</button>
</nav>.none { display: none; }
.hidden { visibility: hidden; }
.opacidade { opacity: 0; }Quem lê a árvore aqui é o ariaSnapshot() do Playwright sobre o <body> — o
mesmo mapa que o leitor de tela percorre:
display: none e visibility: hidden tiraram os botões da árvore. opacity: 0
não: “Agendar vacina” continua lá, anunciado para quem usa leitor de tela e
invisível para quem usa os olhos.
display: contents e o dia em que ele salva um grid
Grid e Flexbox só enxergam os filhos diretos. Quando um componente
envelopa dois cartões numa <div>, essa <div> vira um único item da grade e o
layout desanda. É exatamente o caso da grade de serviços da clínica:
<div class="grade">
<article class="cartao">Consulta</article>
<div class="grupo">
<article class="cartao">Banho</article>
<article class="cartao">Tosa</article>
</div>
<article class="cartao">Vacina</article>
</div>.grade { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; width: 620px; }
.cartao { background: #ddd; padding: 10px; }
.grupo { border: 2px solid #333; }display: contents apaga a caixa do elemento e promove os filhos ao lugar dele.
Uma linha:
.grupo { display: contents; }A medição percorre os quatro cartões e ainda mede a caixa do envelope:
for (const c of document.querySelectorAll('.cartao')) {
const r = c.getBoundingClientRect();
console.log(` ${c.textContent.padEnd(9)} x=${r.left.toFixed(0).padStart(3)} y=${r.top.toFixed(0).padStart(3)} largura=${r.width.toFixed(0)}`);
}
const g = document.querySelector('.grupo').getBoundingClientRect();
console.log(` caixa da .grupo: ${g.width.toFixed(0)} x ${g.height.toFixed(0)}`);Sem contents, “Banho” e “Tosa” estão empilhados na mesma coluna (x=212 os
dois) e sobra uma coluna vazia. Com contents, os quatro cartões viram itens da
grade: três na primeira linha e “Vacina” quebrando para a segunda, em y=52.
O preço está na última linha da primeira medição: a caixa da .grupo saiu de
200 x 87 para 0 x 0. Não existe mais caixa — então a borda de 2px que ela
tinha sumiu junto, e qualquer background, padding ou box-shadow no
envelope some também. display: contents não é um jeito de esconder: é um jeito
de desaparecer com a caixa e manter o conteúdo.
Mais sobre montar a grade em si na lição de CSS Grid.
O display de duas palavras: inline flex é inline-flex
block, inline, inline-block e flex parecem quatro coisas independentes.
Não são. A especificação define display como duas informações separadas: o
tipo de caixa por fora e o tipo de contexto de formatação por dentro.
Os navegadores modernos aceitam essa sintaxe de duas palavras, e traduzem para o
nome antigo na hora de computar. Onze valores escritos, e o que o Chromium
devolve em getComputedStyle:
#t0 { display: block flow; }
#t1 { display: inline flow; }
#t2 { display: block flow-root; }
#t3 { display: inline flow-root; }
#t4 { display: block flex; }
#t5 { display: inline flex; }
#t6 { display: block grid; }
#t7 { display: inline grid; }
#t8 { display: block flow list-item; }
#t9 { display: contents; }
#t10 { display: none; }A quarta linha é a chave da lição inteira: inline flow-root computa como
inline-block. O inline-block que você usa desde sempre não é um valor
especial — é “inline por fora, caixa de bloco independente por dentro”. E
flex, que a gente costuma pensar como o oposto de block, é block flex: por
fora ele é um bloco igual a qualquer <div>; o que muda é só o lado de dentro.
Esse par também explica um comportamento que assusta: pôr um <span> dentro de
um contêiner flex faz ele parar de ser inline sozinho.
<div class="flexpai"><span id="s1">Banho</span><span id="s2">Tosa</span></div>
<div><span id="s3">Vacina</span></div>.flexpai { display: flex; }
#s1, #s2, #s3 { width: 150px; height: 40px; background: #ddd; }O mesmo <span>, com o mesmo CSS: dentro do flex ele computa block e obedece
a width e height; fora, continua inline e ignora as duas. O contêiner flex
força a parte “por fora” dos filhos para block — e é por isso que width
funciona em item de flex sem você ter escrito display nenhum.
flow-root: o clearfix que virou propriedade
Sobra um valor que quase ninguém ensina e que resolve um problema antigo. A ficha do paciente tem a foto flutuando à esquerda:
<article class="ficha">
<div class="foto"></div>
<p class="nome">Thor, 4 anos</p>
</article>
<p class="abaixo">Retorno em 15 dias.</p>.ficha { border: 2px solid #333; width: 400px; }
.foto { float: left; width: 100px; height: 100px; background: #ddd; }
.nome { margin: 0; }Sem correção, o cartão fica com 26px de altura enquanto a foto termina em 102px: a foto escapa por baixo da borda e o parágrafo seguinte sobe por cima dela, começando em 43,59. Elemento flutuado não conta para a altura do pai.
overflow: hidden conserta a altura — e por isso virou remendo padrão por uma
década. Só que ele conserta pelo motivo errado: cria um contexto de rolagem, e
contexto de rolagem recorta o que passar da borda. O selo “urgente”
posicionado 14px acima do cartão paga a conta:
<div class="folga"></div>
<article class="ficha">
<span class="selo">urgente</span>
<div class="foto"></div>
<p class="nome">Thor, 4 anos</p>
</article>A .folga é só um espaçador de 60px acima do cartão — ela existe para haver
alguma coisa atrás do selo, e é assim que a medição descobre se o selo sumiu:
.folga { height: 60px; }
.ficha { position: relative; }
.selo { position: absolute; top: -14px; left: 20px; padding: 2px 8px; background: #333; color: #fff; }const ficha = document.querySelector('.ficha').getBoundingClientRect();
const selo = document.querySelector('.selo').getBoundingClientRect();
// 10px acima da borda do cartão, bem onde o selo aparece
const alvo = document.elementFromPoint(selo.left + selo.width / 2, ficha.top - 10);
console.log('altura do cartão:', ficha.height.toFixed(0) + 'px');
console.log('10px acima da borda:', alvo.className);Este trecho roda três vezes, uma em cada estado do cartão — sem correção
nenhuma, com overflow: hidden e com display: flow-root:
Com overflow: hidden, o ponto onde o selo deveria estar devolve .folga — o
elemento que está atrás. O selo foi recortado fora. Com display: flow-root a
altura é corrigida do mesmo jeito e o selo continua visível, porque nenhum
contexto de rolagem foi criado.
flow-root diz exatamente isso: “por fora eu sou um bloco, por dentro eu abro
uma raiz de fluxo nova”. Nada do lado de dentro vaza para fora, e nada é
recortado. O que overflow: hidden fazia de efeito colateral, flow-root faz
de propósito — sem os efeitos colaterais. Os outros usos de overflow continuam
valendo e estão na lição de overflow.
O que treinar agora
Abra o DevTools numa página que você já fez, clique num elemento e olhe a linha
display no painel de estilos computados. Depois faça o exercício das três
perguntas em cada caixa: ela ocupa a linha inteira? aceita altura? o vizinho
sabe que ela existe? Você acabou de ver a medição das três respostas.
O próximo passo da trilha de CSS é
position — onde as caixas aprendem a sair do fluxo
normal de vez, e onde o display que você escolheu aqui começa a decidir coisas
que não são só tamanho.
Prefere aprender em vídeo?
Tem uma aula sobre este assunto no nosso canal.
Perguntas frequentes
Qual a diferença entre display none e o atributo hidden do HTML?
Posso pôr display block num span e display inline numa div?
display contents já pode ser usado em produção?
Ainda preciso do clearfix com ::after e clear both?
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), e as saídas exibidas são as reais — como produzimos este conteúdo.
Fontes consultadas
- MDN — display — developer.mozilla.org
- MDN — visibility — developer.mozilla.org
- MDN — Flow layout — developer.mozilla.org
- CSS Display Module Level 3 — W3C — w3.org



