Desmistificando o margin collapsing do CSS

Publicado em 3 ago 2020. Uns 11 minutos de leitura.

Um comportamento que de vez em quando me pega desprevinido de CSS é o margin collapsing (literalmente, "desmoronamento das margens", na falta de uma tradução melhor), que é a combinação de duas margens adjacentes em uma só. É algo que faz parte da especificação mas muitas vezes passa despercebido. Agora, quando isso interfere no que queremos fazer, se você não souber o que é e como funciona, vai quebrar a cabeça para desvendar por que suas margens desaparecem. Trouxe a definição de em quais casos suas margens podem sumir e alguns exemplos para ajudar a visualizar cada um!

Vamos começar definindo o que é margin collapsing: é a junção de duas margens adjacentes de CSS em uma só, resultando apenas na maior das margens existindo. Ou seja, nesta situação, uma das duas margens adjacentes vai desaparecer.

Há três situações em que estas margens adjacentes são criadas. Vamos olhar cada uma delas.

Elementos irmãos adjacentes

Se você tem dois elementos imediatamente adjacentes, ou seja, sem nada entre eles, e ambos têm margens que se encostam (por exemplo, o de cima tem margin-bottom e o de baixo tem margin-top), estas margens serão combinadas e uma delas vai se perder (a menor). Vamos ver um exemplo:

<div class="bar"></div>
<div class="bar"></div>
.bar {
  margin: 1em 0;
  width: 10em;
  height: 1em;
  background-color: tomato;
}

No exemplo acima, temos duas divs que têm 1em de altura. Elas também tem o mesmo tamanho (1em) de margem para cima e para baixo. Talvez você esperasse que, como o elemento de cima tem uma margem de 1em para baixo e o elemento de baixo tem uma margem de 1em para cima, que o espaçamento resultante fosse a soma, ou seja, 2em. Isso não acontece, e você pode notar que a separação entre os elementos tem o mesmo tamanho que a altura de cada um eles.

O tamanho das margens não precisa ser o mesmo, como foi no exemplo. Caso as margens tenham tamanhos diferentes, a maior vai prevalecer, como podemos ver abaixo:

<div class="bar top"></div>
<div class="bar bottom"></div>
.bar {
  width: 10em;
  height: 1em;
  background-color: forestgreen;
}

.top {
  margin-bottom: 1em;
}

.bottom {
  margin-top: 2em; /* Esta é maior */
}

Neste caso, a margem resultante foi 2em (e não 3em que seria caso fossem somadas). Essa diferença de tamanho é um pouco mais chata de perceber só olhando, mas se você estiver num PC, use o inspetor do navegador para verificar e confirmar o tamanho da margem resultante.

Pai e filho sem separação

Quando um elemento pai tem uma margem que está adjacente a margem de um elemento filho e não há nada entre essas margens, como uma border ou padding do elemento pai, haverá também a junção das margens. Por exemplo, se temos um elemento pai com margin-bottom e seu último filho também tem margin-bottom, é preciso que haja alguma separação entre as margens para quem as duas existam.

<div class="parent">
  <div class="child">A margem deste filho é maior que a do pai e vai substitui-la.</div>
</div>

<div class="outsider">A margem acima é apenas a o filho, de 2em.</div>
.parent {
  margin-bottom: 1em;
  background-color: lavender;
}

.child {
  margin-bottom: 2em;
  border: solid 3px indigo;
  color: indigo;
}

.outsider {
  border: solid 3px purple;
  color: purple;
}
A margem deste filho é maior que a do pai e vai substitui-la.
A margem acima é apenas a o filho, de 2em.

No exemplo acima, temos um elemento .parent que tem uma margin-bottom de 1em. Seu filho, .child, também tem uma margin-bottom, que neste caso é maior que a do pai (2em). Como nos outros exemplos, a maior margem vai prevalecer. Mas é interessante notar que a margem maior é aplicada ao parente e não ao descendente, ou seja, ela existe do lado de fora do pai. Por isso que a cor de fundo do elemento pai não se estende abaixo do filho.

Se separássemos as margens do pai e do filho, por exemplo, colocando uma borda no elemento pai, essas margens não se juntariam mais. Vamos testar modificando o exemplo anterior:

<div class="parent">
  <div class="child">A margem deste filho é maior que a do pai mas agora elas estão separadas.</div>
</div>

<div class="outsider">Existem duas margens acima, de 2em no filho e 1em no pai.</div>
.parent {
  margin-bottom: 1em;
  border: solid 3px orange; /* Isso separa as margens */
  background-color: lemonchiffon;
}

.child {
  margin-bottom: 2em;
  border: solid 3px peru;
  color: peru;
}

.outsider {
  border: solid 3px sandybrown;
  color: sandybrown;
}
A margem deste filho é maior que a do pai mas agora elas estão separadas.
Existem duas margens acima, de 2em no filho e 1em no pai.

Outros itens que separem as duas margens também impediriam o margin collapsing de acontecer, como um padding no elemento pai, um elemento inline (como um texto) após o elemento filho (mas ainda dentro do pai), uma altura (height ou min-height) no elemento pai que separe as duas margens, e por aí vai.

Um bloco sem conteúdo

Um elemento que esteja vazio, ou seja, que não tenha padding, border, altura ou conteúdo inline como texto, caso tenha margens, estas estarão adjacentes, pois não haverá nada para separar a margin-top da margin-bottom do elemento. Neste caso, o margin collapsing vai acontecer. Vamos de exemplo:

<div class="bar"></div>
<div class="empty"></div>
<div class="bar"></div>
.bar {
  width: 10em;
  height: 1em;
  background-color: steelblue;
}

.empty {
  margin: 1em 0;
}

Neste caso, o elemento .empty possui margem superior e inferior de 1em cada. Porém, como ele não tem nenhum conteúdo, essas duas margens se encostam e viram uma só com o tamanho da maior (como são iguais, a margem resultante é 1em). O resultado é bem similar ao primeiro exemplo, mas dessa vez o margin collapsing aconteceu entre duas margens do mesmo elemento, ao invés de elementos irmãos.

Assim como no exemplo antes deste, se o elemento vazio separar as margens de alguma maneira (e deixar de ser vazio, no caso), as duas passarão a existir separadamente:

<div class="bar"></div>
<div class="not-empty"></div>
<div class="bar"></div>
.bar {
  width: 10em;
  height: 1em;
  background-color: crimson;
}

.not-empty {
  margin: 1em 0;
  height: 1px; /* Isso separa as margens */
  background-color: pink;
}

A mudança no exemplo acima foi apenas a adição de uma altura, neste caso de apenas 1px. Isso é suficiente para separar as duas margens e impedir o margin collapsing. Eu coloquei uma background-color apenas para facilitar a visualização das duas margens separadas, mas ela não faz diferença no margin collapsing. Mesmo sem a cor, as bordas estariam separadas por conta da altura. Um texto dentro do elemento seria também evitaria que as bordas se juntassem.

Alguns detalhes a mais

Nos exemplos deste post, vimos que duas margens imediatamente adjacentes vão ser juntadas em uma só e o tamanho da margem resultante é o maior tamanho entre elas. Mas este efeito não é limitado a duas margens apenas. Podemos, por exemplo, combinar todos os casos acima para mostrar várias margens que vão ser juntas em uma só:

<div class="bar"></div>
<div class="empty"></div>
<div class="empty"></div>
<div class="empty"></div>
<div class="empty">
  <div class="empty"></div>
  <div class="empty"></div>
</div>
<div class="empty"></div>
<div class="bar"></div>
.bar {
  width: 10em;
  height: 1em;
  background-color: limegreen;
}

.empty {
  margin: 1em 0;
}

Pra finalizar, vale mencionar que as margens afetadas por este efeito nem sempre serão positivas. Por exemplo, podemos ter um elemento filho com margem positiva e um elemento pai com margem zero: o margin collapsing vai colocar a margem positiva no pai caso o filho seja o último elemento do pai e não haja nada separando a margem dele do ponto em que a margem (originalmente zero) do pai deveria estar.

<div class="parent">
  <div class="child">Este filho tem margem, mas ela vai ser aplicada no pai.</div>
</div>

<div class="outsider">A margem do filho foi aplicada acima.</div>
.parent {
  margin-bottom: 0; /* Zero */
  background-color: lavenderblush;
}

.child {
  margin-bottom: 2em;
  border: solid 3px mediumvioletred;
  color: mediumvioletred;
}

.outsider {
  border: solid 3px orchid;
  color: orchid;
}
Este filho tem margem, mas ela vai ser aplicada no pai.
A margem do filho foi aplicada acima.

Se você usar o inspetor no exemplo acima, vai ver que a altura do pai não aumentou para acomodar a margem do filho (e, por isso, a cor de fundo do pai não se estende além da altura do filho).

E quanto a margens negativas? Elas existem, e são confusas ao ponto de merecer uma discussão separada só sobre elas. Mas, especificamente no caso de margin collapsing, há duas possibilidades.

Se houver margens positivas e negativas, a margem resultante e a soma da maior margem com a menor margem (a "mais positiva" + a "mais negativa"). No exemplo abaixo, temos como margem resultante entre os elementos 1em + (-2em) = -1em.

<div class="bar top"></div>
<div class="bar bottom"></div>
.bar {
  height: 3em;
}

.top {
  width: 10em;
  margin-bottom: 1em; /* Positiva */
  background-color: lightseagreen;
}

.bottom {
  width: 5em;
  margin-top: -2em; /* Negativa */
  background-color: lightgreen;
}

Se houver apenas margens negativas, a margem resultante é a menor margem (a "mais negativa" entre elas). No próximo exemplo, a menor (mais negativa) entre -1em e -2em vai prevalecer, tendo como margem resultante -2em.

<div class="bar top"></div>
<div class="bar bottom"></div>
.bar {
  height: 3em;
}

.top {
  width: 10em;
  margin-bottom: -1em; /* Negativa */
  background-color: olivedrab;
}

.bottom {
  width: 5em;
  margin-top: -2em; /* Negativa */
  background-color: yellowgreen;
}

Quando você não sabe que margin collapsing existe, ela te pega de surpresa e você pode passar horas para tentar entender pra onde suas margens estão desaparecendo. Mas não tem muito mistério quando você entende o funcionamento! Espero que os exemplos deste post tenham sido mais esclarecedores do que confusos 😅. Se quiser ler mais, a documentação de margin collapsing na MDN fala destes casos e tem links para algumas definições mais específicas do que pode ou não separar duas margens.


Topa um exercício?

Aqui vão alguns exemplos de código para você praticar o que vimos acima, se quiser.

Qual é o espaçamento entre as barras?

<div class="bar"></div>
<div class="bar"></div>
.bar {
  margin: 1em 0;
  width: 10em;
  height: 1em;
  background-color: tomato;
}

O que causaria um espaçamento de pelo menos 2em entre as barras?

<div class="bar"></div>
<div class="empty"></div>
<div class="bar"></div>
.bar {
  width: 10em;
  height: 1em;
  background-color: steelblue;
}

.empty {
  margin: 1em 0;
}

Qual é o resultado deste código?

<div class="bar top"></div>
<div class="bar bottom"></div>
.bar {
  height: 3em;
}

.top {
  width: 10em;
  margin-bottom: -1em;
  background-color: olivedrab; /* Verde-escuro */
}

.bottom {
  width: 5em;
  margin-top: -3em;
  background-color: yellowgreen; /* Verde-claro */
}