CSS: Como `display: contents` funciona

Os benefícios desse novo valor para o layout do dia a dia!

"Se você parar em matemática geral, você só vai fazer dinheiro de matemática em geral." — Snoop Dogg

Cada elemento na árvore do documento HTML é uma caixa retangular. Você já deve ter visto essa afirmação por aí. Essa “caixa retangular” é feita de duas partes. Primeiro, nós temos a caixa em si, que se consiste de border, padding e margin. Segundo, nós temos conteúdo dessa caixa, ou seja, a área do seu conteúdo.

Tudo na web é uma caixa! Como cada “caixa retangular” no HTML é calculada.

Com a propriedade do CSS display, nós podemos controlar diferentes coisas de como essa caixa e seus elementos filhos são desenhadas na página. Nós podemos colocar essa caixa em linha com seus elementos irmãos, como um parágrafo de texto, utilizando inline. Nós podemos transformar o comportamento dessa caixa e fazer com que ela se comporte como uma tabela, utilizando table. E também podemos colocar essa caixa em um lugar completamente diferente no eixo-z com position: absolute.

Existem apenas dois valores, para a propriedade display, que irão controlar se um elemento definido na marcação irá ou não gerar uma caixa. Com o valor none, tanto a caixa, quanto seu conteúdo, não serão desenhados na página. O recém-especificado valor contents, por outro lado, irá desenhar o conteúdo da caixa normalmente, porém, a caixa em torno desse conteúdo será omitida completamente.

O que acontece quando uso `display: contents`?

Uma maneira fácil de entender o que acontece ao usar display: contentsé imaginar que as tags de abertura e fechamento de um elemento, serão omitidas da marcação.

Na especificação, nós temos a seguinte afirmação:

Para fins de geração da caixa e layout, o elemento deve ser tratado como se tivesse sido substituído na árvore de elementos pelo seu conteúdo

Confuso? Vamos ver um exemplo com a seguinte marcação:

<div class="outer">
I’m some content
<div class="inner">I’m some inner content</div>
</div>

E adicionar os seguintes estilos:

.outer {
border: 2px solid lightcoral;
background-color: lightpink;
padding: 20px;
}
.inner {
background-color: #ffdb3a;
padding: 20px;
}

Normalmente, esse é o resultado esperado:

Porém, ao utilizar display: contents no elemento .outer, o resultado abaixo é desenhado na tela:

Visualmente falando, o resultado acima é exatamente o que iríamos esperar se a marcação fosse escrita sem as tags de abertura e fechamento do elemento .outer:

I’m some content
<div class="inner">I’m some inner content</div>

Tudo bem, mas…?

Essa regra CSS, aparentemente simples, tem alguns casos extremos que precisamos observar seu comportamento. Precisamos lembrar que display: contents afeta apenas a caixa que está sendo desenhada visualmente na página e não a marcação do documento.

E os atributos do elemento?

Se o elemento é substituído pelo seu conteúdo, o que isso significa para qualquer atributo aplicado à ele? Como essa substituição é, na maioria das vezes, apenas visual, ainda podemos selecionar, segmentar e interagir com o elemento usando seus atributos.

Podemos segmentar o elemento por seu ID, por exemplo, fazendo uma referência a ele usando aria-labelledby.

<div id="label" style="display: contents;">Label here!</div>
<button aria-labelledby="label"><button>

No entanto, a única coisa que descobri que não funciona corretamente é que não podemos mais navegar até o elemento usando um identificador de fragmento.

<div id="target" style="display: contents;">Target Content</div>
<script>
window.location.hash = "target";
// => Nothing happens
</script>

E os eventos JavaScript?

Como acabamos de ver, podemos selecionar um elemento com display: contents. Uma curiosidade é que, podemos selecionar um elemento com display: none também, mas os eventos nunca serão acionados porque não podemos interagir com ele. No entanto, como o conteúdo de um elemento display: contents ainda é visível, podemos interagir com o elemento por meio de seu conteúdo.

Por exemplo, se definirmos um evento de clique no elemento e chamar console.log com o valor this, iremos obter o elemento externo porque ele ainda existe no documento.

<div class="outer">I’m some content</div>
<script>
document.querySelector(".outer").addEventListener("click", function(event) {
console.log(this);
// => <div class="outer"></div>
});
</script>

E os pseudo-elementos?

Os pseudo-elementos de um elemento com display: contents são considerados parte de seus elementos filhos, portanto, são exibidos normalmente.

<style>
.outer { display: contents; }
.outer::before { content: "Before" }
.outer::after { content: "After" }
</style>
<div class="outer">I’m some content</div>

A marcação acima gerará o seguinte resultado:

E os elementos de formulário, imagens e outros elementos substituídos?

Elementos substituídos e alguns elementos de formulário têm um comportamento diferente quando display: contents são aplicados a eles.

Elementos substituídos

Elementos substituídos são elementos como

, que tem suas “caixas” controladas por um fator externo. Tentar remover a caixa de elementos como este, não faz sentido, porque não está totalmente claro o que é a “caixa” desse elemento. Utilizar display: contents aqui, funciona exatamente como display: none. A caixa inteira e o conteúdo do elemento não são desenhados na página.

Elementos de formulário

Para muitos elementos de formulário, eles não são compostos de uma única “caixa”. Eles se parecem com isso a partir do nosso ponto, os autores de páginas da web. Mas sob o capô, eles são compostos de vários elementos menores.

Da mesma forma que os elementos substituídos, não faz sentido remover a caixa, porque não há uma caixa. E assim, elementos de formulário como , e , utilizar display: contents tem o mesmo resultado que display: none.

(Veja a lista completa dos elementos que display: contents se comporta como display: none)

E botões e links?

Os elementos e não têm nenhum comportamento especial se tratando de display: contents. No entanto, é útil saber como essa regra os afeta, pois pode não ser imediatamente óbvia.

Botões

Botões não são um dos elementos de formulário que são compostos de outras caixas. Portanto, display: contents apenas removerá a caixa ao seu redor, deixando o conteúdo do botão exibido. Se usado dentro de um formulário, clicar no botão ainda enviará o formulário e, como já cobrimos, qualquer evento no botão funcionará normalmente.

Links

Para links, o mesmo se aplica a caixa ao seu redor, ela é removida visualmente, deixando o conteúdo do link para trás. Como os atributos geralmente não são afetados por essa regra de CSS, o link ainda funcionará corretamente e poderá ser usado para navegar normalmente.

Por que é display: contents útil?

No passado, tivemos que expor nosso HTML de uma maneira que funcionasse tanto semanticamente, tanto para fins de estilos com CSS. Isso levou a casos em que temos muitos elementos desnecessários para agrupamentos ou poucos elementos para ativar o estilo de um elemento-irmão direto. Este último, com a introdução do CSS Grid Layout, ficou ainda mais necessário, menos por enquanto, para poder trabalhar com elementos-irmãos diretos.

Vamos pegar, por exemplo, esse layout:

Temos dois “Cards”, colocados um ao lado do outro, cada um com um título, um gráfico e um rodapé. O que queremos é que cada uma das seções dentro de cada “card” tenha a mesma altura, independentemente do conteúdo de cada seção (por exemplo, o primeiro cartão tem apenas uma linha de título, enquanto o segundo cartão tem um título de três linhas, porém, a altura do primeiro título deve coincidir com o segundo e vice-versa).

Poderíamos criar esse layout usando CSS Grid, mas precisaríamos que todos os elementos dentro de cada “card” fossem irmãos diretos uns dos outros.

Então, podemos ter que criar nosso HTML assim:

<div class="grid">
<h2>This is a heading</h2>
<p>...</p>
<p>Footer stuff</p>

<h2>This is a really really really super duper loooong heading</h2>
<p>...</p>
<p>Footer stuff</p>
</div>

Poderíamos aplicar os seguintes estilos:

.grid {
display: grid;
grid-auto-flow: column;
grid-template-rows: auto 1fr auto;
grid-template-columns: repeat(2, 1fr);
grid-column-gap: 20px;
}

Embora essa não seja exatamente uma maneira incorreta de estruturar esse documento, provavelmente faz mais sentido agrupar cada “card” em um elemento. É aqui que display: contents entra. Podemos ter o melhor dos dois mundos — colocando nossa marcação de uma maneira que faça sentido semanticamente, mas fazendo com que nosso CSS se comporte de uma maneira que faça mais sentido para o layout.

<div class="grid">
<article style="display: contents;">
<h2>This is a heading</h2>
<p>...</p>
<p>Footer stuff</p>
</article>
<article style="display: contents;">
<h2>This is a really really really super duper loooong heading</h2>
<p>...</p>
<p>Footer stuff</p>
</article>
</div>

Com o mesmo CSS acima, podemos alcançar o layout que queremos.

Usando `display: contents`

No momento dessa escrita, display: contents só é suportado em dois principais navegadores, com suporte chegando em breve em muitos outros.

https://caniuse.com/#feat=css-display-contents

Por isso, esse recurso ainda deve ser considerado um aprimoramento progressivo e um fallback apropriado deve ser usado.

article {
display: grid;
grid-template-rows: 200px 1fr auto; /* e.g. Use a fixed height for the header */
}
@supports (display: contents) {
article { display: contents; }
}

Créditos