Csswg-drafts: [css-pseudo] Add a ::contents pseudo-element

Created on 6 Mar 2018  Â·  39Comments  Â·  Source: w3c/csswg-drafts

Some years ago I proposed a ::contents pseudo-element. I have been refining the details in https://github.com/Loirooriol/css-contents, and yesterday someone proposed the same idea, so I decided to file this issue.

To summarize, elements generate a ::before pseudo-elements at the beginning of their contents, and an ::after one at the end. This proposal wraps the contents inside a new ::contents pseudo-element.

image

In some way this is the opposite of display: contents: it allows you to insert a box between an element and its children, without needing to change the HTML in non-semantic ways.

You can see all the details and examples in https://github.com/Loirooriol/css-contents, but the behavior is:

  • ::contents has display: contents by default, via a rule in the UA origin. This can be overridden so that it does generate boxes.
  • ::contents can be styled with arbitrary properties.
  • ::contents has no effect in replaced elements.
  • ::contents inherits from its originating element.
  • Elements and text nodes inherit inheritable properties from the ::contents originated by the parent element. For non-inherited properties, inheritance is directly from the parent element (to avoid breaking the inherit keyword).
  • Alternatively, inheritance could be from ::contents for all properties, and assign all: inherit to ::contents in UA origin.
  • This pseudo-element does not affect selectors.
css-pseudo-5

Most helpful comment

Forever running into scenarios where I'd love a targetable block-level text node instead of needing an extra block-level wrapper and sacrificing semantics, especially when using margin in flex/grid contexts.

Really hope this issue evolves before too long. :)

All 39 comments

For what it's worth, similar ideas of inner and/or outer pseudo-elements have been proposed at various times going back at least 19 years. See for example:

The old CSS 3 Content was killed with fire and allowed the creation of pseudo-elements outside the element, which is not consistent with ::before and ::after and is less useful.

Additionally, it allowed nested pseudo-elements, which either meant that they were created by the selector (ugly) or that the element tree was infinite (hard to implement?). I purposely avoided this feature in my proposal, at least for the moment.

I had seen the ::wrap proposal in #588. Frankly, I believe it's completely unrealistic. It seems amazing at first glance, but it allows aberrations like

#parent > :nth-child(1)::wrap(2), /* wrapper #1 */
#parent > :nth-child(2)::wrap(2)  /* wrapper #2 */
{border: thick solid lime}
<div id="parent">
  <div>1</div> <!-- belongs to wrapper #1 -->
  <div>2</div> <!-- belongs to wrappers #1 and #2 -->
  <div>3</div> <!-- belongs to wrapper #2 -->
</div>

So the result is not really a tree! I prefer to avoid this can of worms.

But I believe my ::contents is completely feasible (if there is implementation interest).

Could ::contents be miss-leading? My initial assumption when reading the issue title was that it was a selector for non-element contents (text) inside dom nodes.

If we have ::before and ::after what about ::prepend and ::append? - or ::inside

For me, ::contents doesn't seem to imply non-element contents only, it can refer to _any_ kind of contents of the element (just like it does in the display:contents case). ::inside itself sounds good, but being contrast to ::before/::after it can seem to imply that those two are _not inside_ the element, also leading to the possible confusion. For text-only contents, something with text- in the name would be more straightforward (see #2208).

::prepend and ::append probably should be the better names for ::before and ::after themselves, but renaming them seems now impossible.

@inoas As @SelenIT says the name comes from display: contents, I think it's easy to remember since that's the default value for ::contents. But yes, ::inside or other names could also work.

For now, I think this proposal seems to be comparatively considerate, I cannot find a case to break compatibility with pages designed in the current standards.

Alternatively, inheritance could be from ::contents for all properties, and assign all: inherit to ::contents in UA origin.

That does not sound good. You could always override the ones that you don't like when you turn ::contents { display: /*something other than contents*/} on, but most things would be sort of crazy: double margin, double border, double padding, double outline, double background, double transforms, double float, double clip-path... That doesn't seem it would ever be what you want. I think the original proposal of having the inherit keyword (at least on non inherited properties) go for the actual element is much more useful.

However, sometimes it is desirable to generate content outside the element. This can be accomplished by adding

div { display: contents; }
div::contents { display: block; /* border, margin, ... */ }

This is so cool, I love it.

You probably also should write down how this affects selectors, and in particular the child selector, or things like nth-of-type and the like. My expectation is that it would not affect them at all. If that's the case, it is worth stating, to remove any doubt.

most things would be sort of crazy: double margin, double border

Yes, in this case setting some display value wouldn't be enough to enable the pseudo-element, authors would need to also use all: unset to make it behave properly. Which wouldn't be nice at all, but I thought somebody could prefer this over different inheritance paths for different kinds of properties.

This is so cool, I love it.

Yes, it's one of my favorite features too. Making ::before and ::after behave as siblings can be used to fix all kinds of layouts. That's why generating a box inside the element is more useful than outside (as proposed in the old CSS Content), then we don't need ::before-outside or ::after-outside.

You probably also should write down how this affects selectors

Yes, selectors shouldn't be affected

One of the reasons I keep pushing back on proposals like this is because they break cascading. Once you start styling ::contents, you've forcibly overridden anything on the element itself, even if those rules are higher in the cascade. For that reason, I'd rather we directly solved the problems that are currently being worked around, e.g. we added multiple backgrounds, we are going to add multiple borders, we're adding min-content/max-content/fit-content to width, and we have decided to apply gaps to flex items.

(Sorry, wrong button.)

Once you start styling ::contents, you've forcibly overridden anything on the element itself

Not sure I follow, styling ::contents should only affect ::contents and possibly descendants of the element via inheritance. The styles of the element itself shouldn't change.

we added multiple backgrounds, we are going to add multiple borders

And that's so nice, but there's still only one (principal) box around the descendants. This greatly restricts what one can do in layout-related areas.

@fantasai wrote:

One of the reasons I keep pushing back on proposals like this is because they break cascading

Instead of having a pseudo element, why not introduce a new type of selector that targets only text contents as if it was a wrapped by a tag (directly above or below tag selector specificity).

Let's apply all of those selectors/properties one by one from top to bottom, with @text adding more specificity than * but less than any specific tags; the comments list what happens with the text contents:

section { color: red; }
/* red in section & sub-elems */
section * { color: blue; }
/* red in section, blue in section sub-elems */
section { color: green; }
/* green in section, still blue in all sub-elms */
section p { color: orange; }
/* green in section, still blue in all sub-elms but orange in p as sub-elem */
section @text { color: yellow; }
/* yellow in section and sub-elems but orange in p as sub-elem */
section > @text { color: purple; )
/* purple in section, yellow in sub-elems but orange in p as sub-elem */

...etc.

Obviously applying display modes such as block, list, table, flex, grid, etc would create one block context per combo-wrap of text nodes and inline, inline-block, etc elements. By doing so you can mix in say a, span, abbr, code, etc elems. nth-child() etc. would work on those virtual @text elements, too.

In other words @text would work a bit like a p-tag that auto opens new text-nodes for any non-whitespace (and non obviously element) text and auto-closes before any block context as well as closing of the wrapping element.

Is there already a display mode for text? is inline fitting 100%.

@inoas I also support adding a pseudo-element to select text nodes (#2208). There is overlap in the case of an element which only contains text, but other than this ::text and ::contents are different features.

Is there already a display mode for text?

Text has no display type but in https://github.com/w3c/csswg-drafts/issues/2208#issuecomment-359468226 I proposed display: contents for ::text.

@Loirooriol Given that my suggestion above does not break the cascade (does it?) where would you still require ::contents once you have @text (note that I am not using colons as that would imply that it applies to elements, @text would instead simply select text nodes as if they were first class elements.)


Edit:

I say to mimick this PR's elem::contents here, you could do:

elem > *,
elem > @text {
  …
}
/* applied to all children nodes, wether they are regular elements or text nodes */ 

If you simply want to add wrappers with @fantasai on extending css properties. I am also a proponent of adding more semantic unique tags instead of adding divs as polymorphic containers. That way even with semantic structures you have an easier time to add wrapping styles.

@inoas OK, I didn't read properly your previous comment. Now I think I understand the "break cascading" problem. In fact I would probably recommend against using *::contents or *::text after a descendant combinator to set inheritable properties. I would transform your example into

section { color: red; }
/* red in section & sub-elems */
section * { color: blue; }
/* red in section, blue in section sub-elems */
section { color: green; }
/* green in section, still blue in all sub-elms */
section p { color: orange; }
/* green in section, still blue in all sub-elms but orange in p as sub-elem */
section::text, section * { color: yellow; }
/* yellow in section and sub-elems but orange in p as sub-elem */
section::text { color: purple; )
/* purple in section, yellow in sub-elems but orange in p as sub-elem */

But I don't really see this as a new problem against ::contents and ::text. If someone uses

*::before { color: blue }
very#specific.selector { color: red }

then very#specific.selector::before will have blue color despite very#specific.selector's specificity, but this doesn't stop ::before and ::after from being useful nor being used all over the place.

Do you agree that having an element-style selector instead of a pseudo element type selector is more flexible as you can apply the cascade or use direct descendant operator/selector?

Where would you see benefits of having ::contents over @text?

Do you agree that having an element-style selector instead of a pseudo element type selector is more flexible

I agree that exploring element-style selectors for tree-abiding pseudo-elements may be interesting. But I think of it as a separate selector-specific feature, not intrinsically related to nor as a replacement of ::text or ::contents.

In fact I don't understand why you think @text would be better than ::text. Even if section @text has less precedence than section p, the text inside the paragraph will be matched by section @text and use its color instead of section ps one, right?

Where would you see benefits of having ::contents over @text?

  • I may want to take some element out-of-flow like in absolutely positioning, but still reserve some space where it would be with static positioning. Easy:

    #target { width: 100px; height: 100px; }
    #target::contents { display: block; position: absolute }
    
  • I may want to generate ::after outside the element, e.g. the element is a flex item and I want ::after to participate in the same flex formatting context. Easy:

    #target { display: contents }
    #target::after { content: "" }
    #target::contents { display: block }
    
  • I may want the contents of an element to be wrapped in a stacking context separated from ::after, e.g. because I want to ensure ::after will overlap the contents even if there is some descendant with z-index: 99999999999999. Easy:

    #target::contents { display: block; position: relative; z-index: 0 }
    #target::after { content: ""; position: relative; z-index: 1 }
    

Etcetera. ::content can be used in all cases where text is not intrinsically involved. ::text would only work if the element in question only contained text nodes.

Where would you see benefits of having ::contents over @text?

I may want to take some element out-of-flow like in absolutely positioning, but still reserve some space where it would be with static positioning. Easy:

#target { width: 100px; height: 100px; }
#target::contents { display: block; position: absolute }

And where is that a beneficial (more flexible/reusable / easier to grasp / shorter) syntax over @text?:

#target { width: 100px; height: 100px; }
#target > @text { display: block; position: absolute }

I may want to generate ::after outside the element, e.g. the element is a flex item and I want ::after to participate in the same flex formatting context. Easy:

#target { display: contents }
#target::after { content: "" }
#target::contents { display: block }
#target { display: flex }
#target > @text { color: red; /* red flex item(s) */ }
#target::after { color: green; content: "green flex item after" }

@text does a similar thing?

What would happen in case of ::contents with sub-elems of #target that become flex items by default? Would there exist multiple ::contents nodes in between those?

I may want the contents of an element to be wrapped in a stacking context separated from ::after, e.g. because I want to ensure ::after will overlap the contents even if there is some descendant with z-index: 99999999999999. Easy:

Note sure I got the use case.

Don't get me wrong, I rather see ::contents live than nothing to happen, but I want to make sure that we can't have a more general purpose solution than that, that also follows the cascade and has a clear place in specificity order. Also @text may be a bad selector name for those anonymous text nodes. It is just a name for now.


Edit: I re-read your proposal and see now you consider ::contents as a wrapper around all sub elems including text nodes, maybe in a sense of ::around (before, around, after).

@text does a similar thing?

I understand that @text/::text only contain text. Imagine I have

<section>
  <div>
    This <span>contains</span> a mix of <i>text</i> and <b>elements</b>
  </div>
</section>
section { display: flex }
div { display: contents }
div::before, div::after { content: "" }
div::contents { display: block }

results in this box tree:

  • <section>: flex container

    • <div::before>: flex-level block container

    • <div::contents>: flex-level block container



      • This: text-run


      • <span>: inline element


      • contains: text-run


      • ...



    • <div::after>: flex-level block container

Using ::text would result in

  • <section>: flex container

    • <div::before>: flex-level block container

    • <div::text#1>: flex-level block container



      • This: text-run



    • <span>: flex-level block container



      • contains: text-run



    • <div::text#2>: flex-level block container



      • a mix of: text-run



    • ...

    • <div::after>: flex-level block container

Completely different.

Here would be an idea for §text and §inline pseudo-elements. §inline would match what ::contents tries but split into multiple wherever there are block contexts inside.

<div>
  text1
  <span>
    text2
  </span>
  text3
</div>
<style type="text/css">
  div > §inline { /* targets text1, span/text2, text3 as one node.
    border 1px solid red would add one border around everything as all nodes are inline */ }
  div §inline { /* as above but,
    text2 inside span would receive another border because it is an inline node */ }
  div > §text { /* targets text1 and text3 as separate nodes.
    border 1px solid red would add one border around text1, one around text2 */ }  
  div §text { /* As above but because of the cascade
    text2 will also have border 1px solid red applied, not only text1 and text3 */ }  
</style>

Link https://gist.github.com/inoas/8c66373a9f41e65fab988cb88feb7960


However I can see the difference as ::contents does not care about the sub types of the box tree.
I will eventually open a separate issue.

Features don't really do well without implementor interest, and in this case we have clear implementor anti-interest - @tabatkins https://github.com/w3c/csswg-drafts/issues/588

After reading through the history of this type of feature, I'm left feeling like this most recent proposal solves nearly every issue brought up over the years. And that it's pretty much unanimously a desired feature, except by implementors. @Loirooriol I'm sure you have a lot on your plate, but wondering what you think needs to happen next for this to move "forward"? And if anyone could get preliminary feedback from implementors?

i.e. I can't wait to use this.

@jonjohnjohnson I guess a Pseudo-Elements L5 draft should be created, and include my proposal there. This probably needs a CSSWG approval, and implementors may not like it. Also Pseudo-Elements L4 doesn't seem to be actively maintained, so maybe a new editor?

In light of replaced content not supporting pseudo elements like ::before/::after would you think ::contents could still apply? I can imagine its application being quite useful for wrappers around images with unique object-fit/position, as well as shadow dom implementations of "file" type inputs.

Nah, replaced elements wouldn't have a ::contents.

Forever running into scenarios where I'd love a targetable block-level text node instead of needing an extra block-level wrapper and sacrificing semantics, especially when using margin in flex/grid contexts.

Really hope this issue evolves before too long. :)

What can be done to move this forward? It's now more than ten years of the supposed Semantic Web(tm) and CSS's capabilities are inadequate to optimally utilize HTML's semantic elements.

Recommending the use of semantic elements while CSS's deficiency in this regard regularly requires a wrapping element (div) hack is a confusing omission in the design of HTML5(tm).

I had seen the ::wrap proposal in #588. Frankly, I believe it's completely unrealistic. It seems amazing at first glance, but it allows aberrations like

#parent > :nth-child(1)::wrap(2), /* wrapper #1 */
#parent > :nth-child(2)::wrap(2)  /* wrapper #2 */
{border: thick solid lime}
<div id="parent">
  <div>1</div> <!-- belongs to wrapper #1 -->
  <div>2</div> <!-- belongs to wrappers #1 and #2 -->
  <div>3</div> <!-- belongs to wrapper #2 -->
</div>

So the result is not really a tree! I prefer to avoid this can of worms.

But I believe my ::contents is completely feasible (if there is implementation interest).

I'm not sure I understand you. Your CSS code example is pretty normal should not lead to something you showed in your HTML code example. Maybe there is some misunderstanding.

I had a go at implementing this on an idle day a few weeks back, just to see how it would work. I think the concept is quite clean overall and, of all the proposed pseudo-element approaches this one seems the most coherent.

But I think there's some clarification required around the interaction of the ::contents pseudo-element and content: contents, in particular when it's used to move the content of an element to a pseudo-element, like so:

span::before {
    content: '(A=' contents ')';
}
span {
    content: none;
}
span::after {
    content: '(B)'
}
<span>test</span>

```
(A=test)(B)

(For those of you raising an eyebrow at this: here's the [content](https://www.w3.org/TR/css-content-3/#content-property) property definition)

My question is, if I then add a new style with the ::contents pseudo-element, where does it go?
```css
span::before {
    content: '(A=' contents ')';
}
span {
    content: none;
}
span::after {
    content: '(B)'
}
span::contents {
    border: 1px solid red;
    padding: 0px 5px;
}
<span>test</span>

There are two options here.

  1. The pseudo-element is placed between the ::before and ::after pseudo-elements, but has no content. You get the following boxes (and an empty red box):
<span><::before>(A=test)</::before><::contents></::contents><::after>(B)</::after></span>
  1. The ::contents pseudo-element always wraps the "contents", even if it's moved to a pseudo-element. You get the following boxes (and a red outline around "test"):
<span><::before>(A=<::contents>test</::contents>)</::before><::after>(B)</::after></span>

As @Loirooriol's draft proposal is now, you get option 1. Personally I think option 2 is more useful, however there are some caveats:

  1. The end result is a pseudo-element inside a pseudo-element. But, very importantly, this is only for layout. Styling is unaffected: ::before::contents would not match anything.

Actually that's all I've got. If we can get this resolved one way or another, I'll have another go at implementing.

I'd also like to point out that the ::contents element is conceptually almost identical to the https://drafts.csswg.org/css-regions/#regions-flow-content-box. So there's precedent. Sort-of.

Oh there is another point I wanted to make.

I think it might be a lot easier if the descendants don't inherit from ::contents. That should get you past a lot of the concerns. Elements are styled according to their location in the DOM, exactly as they are now, and a ::contents pseudo-element does not affect this.

This approach is consistent with content: contents. An example:

span::before {
    color: red;
    content: '(A=' contents ')';
}
span {
    content: none;
}
span::after {
    content: '(B)'
}
<span>still in black</span>

In the box tree, the text content of the <span> is a child of the ::before, but it doesn't inherit any style. And of course it's the same approach taken with all the other box fixups which are not visible to the author; e.g. moving blocks out of inlines affects the box tree, but not the styling.

Put another way: almost nothing inherits from ::contents. I say almost because if you do this:

span::before {
    color: red;
    content: '(A=' contents ')';
}
span {
    content: none;
}
span::contents {
    color: blue;
    border: 1px solid red;
    display: inline list-item;
}
span::after {
    content: '(B)'
}
<span>test</span>

Then the ::contents is getting generated content of its own. I think the result here should be: (A=•test)(B), where (A is red, • is blue, test is black, ) is red, (B) is black, and there is a red border around "•test"

When I say "we had a go at implementing it", this was the approach we took.

I think it might be a lot easier if the descendants don't inherit from ::contents

It has happened multiple times that I wanted to style the contents of an element, but excluding ::before and ::after. I was trying to cover this usecase.

I guess what you are proposing is that children elements in the DOM would remain being children in the element tree, they would just generate boxes inside ::contents.

I agree this makes inheritance simpler, but seems less useful and it's not clear where ::contents would fit in the element tree. Between ::before and the first child? Between the last child and ::after?

I guess what you are proposing is that children elements in the DOM would remain being children in the element tree, they would just generate boxes inside ::contents.

That's my working mental image, yes. I've had another read through your full proposal, I think it's exactly the box model you proposed, just without the impact on inheritance. So I'm not really clear on what you mean by _where it would fit in the element tree_ - you mean for styling? As a pseudo-element it would inherit from it's owning element, exactly as for ::before. I suspect I've misunderstood the question!

Here's another way to phrase what I'm suggesting.

The ::contents pseudo-element is auto-generated to surround the contents value of the content property. If contents is not used in the content property of the element or its ::before/::after pseudo-elements, the ::contents pseudo-element is not generated (which is the case for replaced content). The content property does not apply to the ::contents pseudo-element.

The element's default value of content: normal computes to content: contents, which means by default the box would be generated between the ::before and ::after pseudo-elements. And, as the ::contents pseudo-element defaults to display: none, the box is not generated by default, and we have the existing rendering model.

I've looked through the examples from your proposal and I think this will work for all of them. If there are cases where you think it's less useful, lets take a look and see if we can figure them out. But I am struggling to imagine a case where you'd want the element's children to inherit styling from the ::contents pseudo-element.

The best example I could come up with that you haven't illustrated already was automatically generating table cells:

td {
    display: contents;
}
td::before {
    content: attr(header);
    display: table-cell;
}
td::content {
     display: table-cell;
}

<td header="foo">bar</td>

to give the box model

<td::before>foo</td><td::content>bar</td::content>

And, as the ::contents pseudo-element defaults to display: none

I assume you mean contents as in @Loirooriol's OP.

If my idea of ::background and ::foreground or generic ::layer() pseudo-elements ever gained any traction, ::contents (as defined in this issue) would probably be equivalent to ::layer(0), because it would be the planar and the stacked middle(ground).

I would like to suggest that, like ::before and ::after, which are not selectable through other CSS selectors, ::contents should not be selectable through other CSS selectors as well.

And can we just use the singular form (::content) to name it?

automatically generating table cells:
```
td {
display: contents;
}
td::before {
content: attr(header);
display: table-cell;
}
...

bar

Maybe it's worth noting that, technically, this example already _kind of_ works due to automatic box tree fixup algorithm of the table layout (with "bar" becoming an anonymous table-cell box next to the table-cell box generated for ::before). The only thing that is currently impossible (but would become possible with the new proposal) is the direct styling of that cell.

@ianthedev See https://drafts.csswg.org/selectors/#pseudo-element-syntax

Pseudo-elements are featureless, and so can’t be matched by any other selector.

I got into this "issue" by performance problems in an animation. I want to blur out the page content when an overlay is shown. The page gets a blur class attached when the overlay is shown. (I tried using backdrop-filter on the overlay before but performance was horrible with that one!)

#page {
  opacity: 1;
  transition: opacity 1s;
}
#page.blur {
  opacity: .3;
  filter: blur(10px);
}

The logic is simple: It should apply the blur filter once and then fade out the hole thing smoothly. But what I get is a horrible stuttering; the blur filter gets recalculated for each frame. If I hovever put the hole #page content into another div and apply the filter only to that, everything is smooth again.
I do not know if this is a design issue in Chromium or if this wrapper is the correct way of doing it here. In both cases it seems quite obvious to me that adding just another div inside the HTML hierarchy just to improve the performance seems ridiculous; really supporting this ::content thingy, if not to support quick hacks like those.

Was this page helpful?
0 / 5 - 0 ratings