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.
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.::contents
originated by the parent element. For non-inherited properties, inheritance is directly from the parent element (to avoid breaking the inherit
keyword).::contents
for all properties, and assign all: inherit
to ::contents
in UA origin.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, andassign 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 p
s 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 containerThis
: text-run<span>
: inline elementcontains
: text-run<div::after>
: flex-level block containerUsing ::text
would result in
<section>
: flex container<div::before>
: flex-level block container<div::text#1>
: flex-level block containerThis
: text-run<span>
: flex-level block containercontains
: text-run<div::text#2>
: flex-level block containera mix of
: text-run<div::after>
: flex-level block containerCompletely 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.
<span><::before>(A=test)</::before><::contents></::contents><::after>(B)</::after></span>
<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:
::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 thecontent
property. Ifcontents
is not used in thecontent
property of the element or its ::before/::after pseudo-elements, the ::contents pseudo-element is not generated (which is the case for replaced content). Thecontent
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.
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. :)