The paradoxes of “display:contents” and the future of text in CSS

This is a translation of an existing article in Russian on css-live:

One more time about display:contents

We already know how display:contents works. If you’re still ignoring it, that’s a shame: there are many reasons why it can be very important. First of all — it’s one of the best solutions to lack of subgrid, a way to avoid damaging the code just for the sake of throwing in a bunch of blocks in one grid, and to expand the formatting possibilities without ruining the semantics (and therefore, also the availability). Secondly — display:contents is the default style for the <clot> element according to the new shadow DOM spec, which is the basis of native web components. And lastly — if previously it used to work only in one browser (Firefox), then now it works also in Chrome. Still filed under «experimental web platform features», but it works (although canIUse and MDN are insisting to keep quiet about it!:). And for the previously mentioned <slot element> — also in Safari (and it’s about to get expanded for all the other elements too). These aren’t the only reasons, but, I think we can agree it’s already enough to deserve having a better look at.

The function of this value may seem unusual, but it is logical. The children of the element go directly to its place, as if they’re the children of its parents, while the element itself disappears. Inside the children everything stays the same. Only the nesting seems to chance. Nothing too hard, huh?

“Unusual” elements and display:contents

What’s going to happen if we apply display:contents to a replaced element, like <iframe> or <object>? Or a form element, like <textarea> or <button> (just in case: in HTML form elements are considered non-replaced elements)? And what’s going to happen to SVG elements ilke <use>?

When it comes to empty elements, like <img> or <input>, everything is more or less clear: no content — no children that you can raise to some sort of different level, so for them display:contents would have just as much effect as display:none (even though, considering shadow DOM, as the article about tricky pseudo elements showed on the example of not fully loaded <img>’s, even this is not that obvious). But elements like <object>, <textarea> and <button> do have content, so this is a different story. But <object>’s content appears only if the main function did not work, and <textarea>’s content doesn’t bear much importance unless it’s edited, therefore, it doesn’t have to be shown. So what do we do?

This is probably what Tab Atkins — who proposed the idea of display:contents for the W3C CSS Display — didn’t think about. And later he and many others spec editors, together with browser developers, had to scratch their heads over it. In the end they had to do as usual: add a special appendix into the specs where its individual behavior with each of those “tricky” elements is listed.

To sum it up, what we ended up with is that most form and replaced elements — not only empty ones (<img>, <input>, <embed>), but also the ones whose display has some sort of “special browser magic” (such as <object>, <textarea>, <iframe>, <video>, <canvas>, <select> and a few more) are hidden completely (like with display:none). <legend> has its content displayed but appears like completely regular text — with no tricks of being placed into a frame break. The contents of <button>, <fieldset> and <details> are displayed as well, as they are — without the “special effects” which are applied to these elements by default.

So with display:contents there’s hope to overcome the illogical limitation of <button> — the inability to give it a display:inline. The button stubbornly insists to remain “its own thing”, like with inline:block. Many coders who are forced by various life circumstances to style <button> and <a> the same way (let’s put aside the fight over whether it’s an anti-pattern or not), endlessly curse this characteristic, often completely rejecting a proper <button> in favor of the imperfect but obedient <a href=”#”>. Now the button contents can finally be displayed like a regular text — with no “magical” borders. But this text will remain clickable and will send the form or perform any other function properly — because display:contents affects only the form of display, without affecting the DOM! Moreover, it already works in Chrome (58+ with the flag turned on)!

In Firefox, sadly, the button’s “magic” is still too strong.. But lately it’s being worked on a lot, so this problem as well should be solved soon.

Among the SVG-elements the ones who stick out the most are <g>, <tspan> and <use>. What’s being displayed in their place is their content (instead of groups — a set of individual group elements, instead of the title — separate text elements). <use>’s case is even more interesting: What’s being displayed in its place is not its “true” content — which it doesn’t even have — but the shadow (a copy of the connected element, as if it really was placed in there). For all the other elements, be it SVG or HTML, the shadow is ignored! As for the rest of the SVG elements — including <svg> itself — display:contents is the same as display:none, hiding them completely.

Pooh! The irregular elements are more or less sorted out. But here comes the next twist…

Inheritance and display:contents

Yes, display:contents has enough surprises even with regular elements. What do you think, what’s going to be the result of this example?

div {
color: blue;
p {
display: contents;
color: red;

What colour should “One” be? Red, like its parent <p>? But that <p> is not part of the visual structure, the render tree, both <span>’s end up on the same level, directly inside <div>. So, blue?

Luckily, both the spec and the browser here act the same way: in CSS inheritance always goes according to the DOM tree (there are rare exceptions, for example, the pseudo-element ::first-line and anonymous boxes, but it’s not about them). If a certain hierarchy level is invisible — that’s not a problem, it can still be inherited from. After all, you can inherit border-spacing’s of tables from non-table parents (which is very handy for code formatting, generated by the users). So the text will be red.

Easy? At first glance, yes. That is, until we take a look at the behavior of the…

Text properties and display:contents

The following example seems to be even easier than the previous:

section {
color: red;
div {
color: green;
display: contents;

What colour should be the “Text”? It’s not an element, it’s an anonymous inline box. The spec used to say that these things inherit the properties of their parents by the render tree. The div is not part of the rendering, the text there is positioned directly inside <section>. So, it’s red? But what if we change the code a bit?


The <span> element, as we already know, inherits its properties from the div (even if the div itself isn’t displayed), so in this case the text is definitely green.

Because of this simple example the spec editors almost got scared themselves. Elika Etemad (aka fantasai) decided that yes, in the first example the text has to be red. But Tab Atkins argued that a text that magically changes colours after the addition of a simple, non-styled element — is a tiny bit too much (and I can understand him:).

But if in both cases the text is green (which, by the way, actually happens in Firefox and Chrome with the flag turned on) — where does the colour property inherit from in the first case then? The anonymous inline box itself?

Let’s suppose so. But what should we do about the next example then?

div {
display: flex;
flex-direction: column;
span {
color: red;
display: contents;

How many flex elements should there be? One (the text from <span> and the text of its parent will both go into the main anonymous inline box)? But how will the “Two” text inherit its colour then? Two (each of the coloured texts is an anonymous inline box on its own, which will become a separate flex element)? But then the result will be the same as without display:contents, the <span>’s content will remain inside its borders, the visual hierarchy won’t change. And the main idea of the whole thing is the possibility of changing it!

Luckily, the browsers all seem to agree on displaying both lines as a single flex element (or as a single grid element, if you switch the display of the container to grid — the main logic of both schemes is similar). In other words “One” and “Two” do go into the same inline element. But their styles are still different!

That’s the paradox.

To be fair, differently styled text fragments in one inline element were already found in CSS in the past. For example, if some kind of symbol wasn’t included in the main font and it was displayed using the default font: in this case, with line-height: auto, the actual line-height (which we get by multiplying the font metrics by the font-size and is used when being displayed on the screen) could easily be different from its neighbours. But the specified values of all the CSS properties in the range of one inline element (especially an anonymous one) were always identical until the arrival of display:contents.

So here’s our case. We got some absolutely new object “run of text”…

  • which is very well compatible with browsers;
  • which can inherit the parent’s properties by itself;
  • which is not a CSS block (neither regular, not anonymous);
  • and which is… not described in any spec!

The spec editors were not prepared for such a twist. Because a new text status — not a “poor relative”, but a full part of the DOM tree — should be presented at least:

  • in the CSS Display module itself — to point out that DOM has not only elements, but also texts;
  • in the CSS Cascade module — to determine the inheritance rules for it;
  • in the CSS Scooping module — to clarify what happens to the text in a shadow tree (web components once again!);
  • in the CSS Text module — we should, after all, define the term itself more strictly;
  • in the CSS Text Decoration module — to clear up the nuances of the text inheritance styling.

And this is just what Tab Atkins remembered straight away. So most likely — in a bunch of other places as well.

And all of the listed specs are already fully functioning. It would be great if this will help to clear up some other strange aspects CSS has, related to text. That’s how things are with CSS: you want to add one seemingly simple thing — and in the end it could result in all of us having to relearn CSS all over again!

And they say specs are boring, man…:)