Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[css-pseudo] Add a ::contents pseudo-element #2406

Open
Loirooriol opened this issue Mar 6, 2018 · 54 comments
Open

[css-pseudo] Add a ::contents pseudo-element #2406

Loirooriol opened this issue Mar 6, 2018 · 54 comments

Comments

@Loirooriol
Copy link
Contributor

Loirooriol commented Mar 6, 2018

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.
@dbaron
Copy link
Member

dbaron commented Mar 7, 2018

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:

@Loirooriol
Copy link
Contributor Author

Loirooriol commented Mar 7, 2018

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).

@inoas
Copy link

inoas commented Mar 7, 2018

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

@SelenIT
Copy link
Collaborator

SelenIT commented Mar 7, 2018

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.

@Loirooriol
Copy link
Contributor Author

@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.

@Zhang-Junzhi
Copy link
Contributor

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.

@frivoal
Copy link
Collaborator

frivoal commented Mar 8, 2018

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.

@frivoal
Copy link
Collaborator

frivoal commented Mar 8, 2018

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.

@frivoal
Copy link
Collaborator

frivoal commented Mar 8, 2018

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.

@Loirooriol
Copy link
Contributor Author

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

@fantasai
Copy link
Collaborator

fantasai commented Mar 8, 2018

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.

@fantasai fantasai closed this as completed Mar 8, 2018
@fantasai fantasai reopened this Mar 8, 2018
@fantasai
Copy link
Collaborator

fantasai commented Mar 8, 2018

(Sorry, wrong button.)

@Loirooriol
Copy link
Contributor Author

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.

@inoas
Copy link

inoas commented Mar 8, 2018

@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%.

@Loirooriol
Copy link
Contributor Author

@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 #2208 (comment) I proposed display: contents for ::text.

@inoas
Copy link

inoas commented Mar 8, 2018

@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.

@Loirooriol
Copy link
Contributor Author

@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.

@inoas
Copy link

inoas commented Mar 8, 2018

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?

@Loirooriol
Copy link
Contributor Author

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.

@inoas
Copy link

inoas commented Mar 9, 2018

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).

@Loirooriol
Copy link
Contributor Author

@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.

@inoas
Copy link

inoas commented Mar 11, 2018

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.

@jonjohnjohnson
Copy link

jonjohnjohnson commented Jun 25, 2018

Features don't really do well without implementor interest, and in this case we have clear implementor anti-interest - @tabatkins #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.

@Loirooriol
Copy link
Contributor Author

@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?

@faceless2
Copy link

faceless2 commented Feb 1, 2020

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 property definition)

My question is, if I then add a new style with the ::contents pseudo-element, where does it go?

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.

@faceless2
Copy link

faceless2 commented Feb 1, 2020

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.

@Loirooriol
Copy link
Contributor Author

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?

@faceless2
Copy link

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>

@ByteEater-pl
Copy link

ByteEater-pl commented Feb 16, 2020

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

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

@Crissov
Copy link
Contributor

Crissov commented Feb 16, 2020

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).

@ianthedev
Copy link

ianthedev commented Feb 17, 2020

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?

@SelenIT
Copy link
Collaborator

SelenIT commented Feb 17, 2020

automatically generating table cells:

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

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

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.

@Loirooriol
Copy link
Contributor Author

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

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

@L3P3
Copy link

L3P3 commented May 12, 2020

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.

mjumbewu added a commit to appropriatetech/icat9 that referenced this issue Sep 30, 2020
I was trying to see how far I could get with avoiding wrapping elements 
to achieve sections as full-width bands. There are some exciting CSS 
things being discussed:

* A [::wrapper pseudo 
element](w3c/csswg-drafts#588) seems stalled 
because it could cause some tricky situations in the DOM, but
* A [::contents pseudo 
element](w3c/csswg-drafts#2406) is still under 
consideration and has some steam.
@mayank99
Copy link

6 years later, this feature has become even more desirable, thanks to container queries.

Container queries, as the name implies, can only query containers. Today this means we often need to add a wrapping DOM element to be able to query it.

With something like ::contents, we'd be able to query the same element!

.thing {
  container-type: inline-size;

  &::contents {
    @container (width < 20ch) {…}
  }
}

This pattern would similarly be incredibly useful for container style queries.

button::contents {
  @container style(--variant: cta) {…}
}
<button style="--variant: cta">

@meyerweb
Copy link
Member

Container queries, as the name implies, can only query containers. Today this means we often need to add a wrapping DOM element to be able to query it.

With something like ::contents, we'd be able to query the same element!

I have a related use case: wanting to set lengths based on a container’s size, for the container itself. I tried to do this:

#container {
   container-type: inline-size;
   font-size: 3cqw;
}

I ended up wrapping the content of the container in a div and setting the font size there instead:

#container {
   container-type: inline-size;
}
#container div {
   font-size: 3cqw;
}

The div has literally no other purpose than to make this possible.

@fantasai explained to me that there would be major cascade problems with a pseudo-element like this, but as there are use cases for being able to do things like what @mayank99 and I described, it would be good to have a way to make them possible without adding elements.

@mirisuzanne
Copy link
Contributor

While the cascade concern makes some sense to me, it also strikes me as saying "if you can put a div inside the element, and style the nested div, that breaks the cascade". The only change here is being able to style a nested box without changing the HTML source. If the ::contents pseudo-element is clearly exposed in developer tools, the behavior (and the debugging) are identical to nested elements.

@kizu
Copy link
Member

kizu commented Jun 10, 2024

One thing I was thinking about regarding the ::contents — we probably don't want to create it for every element by default.

And while the proposed “::before and ::after being outside ::contents” really unlocks various good use cases, I wonder if we'd want to have an ability to choose.

What if we had a non-inherited property that would enable the ::contents on any element (and will do nothing on pseudo-elements), like (naming is probably the hardest) enable-contents. It would have an initial value of none, and a normal for what we could deem as the default behavior. Plus three values that include or exclude (based on that default behavior) ::after and ::before or both from the ::contents.

The ::contents pseudo-element will have display: contents by default, of course.

This won't affect any existing code, and we'd need to explicitly enable the pseudo-element on the elements where we'd want it. It would behave just as any other element, in that anything inside of it will inherit from it, not from the element itself.

Someone could do * { enable-contents: normal } and get some weird consequences, but I don't think there is any reason to prevent this. But if we'd want to do so, then making the ::contents not to have display: contents, but be either block or inline by default could solve it (as this will obviously break almost everything when enabled on *).

I would be curious to know what are the potential issues (cascading or otherwise) with this kind of approach.

@mirisuzanne
Copy link
Contributor

If the ::before and ::after pseudos can be created by adding a property internally, couldn't display be the generating property on ::contents?

@kizu
Copy link
Member

kizu commented Jun 10, 2024

If the ::before and ::after pseudos can be created by adding a property internally, couldn't display be the generating property on ::contents?

I like that idea. And then, the default style for it could be ::contents: { display: contents }, with the condition that for this value the pseudo-element is not generated.

Although, there might be cases where someone could want to have such an element, but the only thing I can think of is something like “I want to pass some inherited property down to the children without actually applying it on the element itself”. This could be mostly worked around via > * and inherit() (but not completely), so I think it is ok to sacrifice this use case.

With display values besides contents creating the pseudo-element there won't be a *::contents { } value that people could shoot themselves in the foot, I don't think so?

@Loirooriol
Copy link
Contributor Author

What are you trying to achieve by requiring a new property? The only advantage that I see is that it would allow inheriting as normal, without requiring all: unset on ::contents either. But you still need a declaration on another rule, so it seems less convenient than my ideas.

Note that even if we say that ::contents always exists, browses can optimize it out if it gets no style (just like ::before) so it wouldn't waste memory. And since the default is display: contents, then it generates no box by default.

@kizu
Copy link
Member

kizu commented Jun 10, 2024

If we'd just use the display for toggling it on, then the only reason to have a separate property could be if we'd want to control if the pseudo-element should include the ::before and ::after.

I am not a browser engineer, and if it could be done in a way that every element will inherently have a ::contents always applied without any performance or other issues — sure, why not. But it is unclear to me why is it useful to have them always be generated, compared to generating them conditionally?

@Loirooriol
Copy link
Contributor Author

The advantage of always generating it is that if you want to use it, it's easier. Compare:

  • With custom inheritance, you just need to use what you want:
    #a::contents { color: blue }
    #b::contents { display: block }
  • With all: inherit default, you may need all: unset if you generate a box:
    #a::contents { color: blue }
    #b::contents { all: unset; display: block }
  • With enable-contents property, you need an entirely different rule:
    #a { enable-contents: yes }
    #a::contents { color: blue }
    #b { enable-contents: yes }
    #b::contents { display: block }

Not a big fan of basing the existence on display: contents in a way that affects inheritance, since this isn't something that contents typically does, and we would lose the ability to just set inherited properties without generating a box.

@kizu
Copy link
Member

kizu commented Jun 10, 2024

My preferred way would be no boxes by default, and thus no custom inheritance or default all:

#a::contents { display: contents; color: blue;  }
#b::contents { display: block }

If we could have some non-contents value as the initial non-generating value and allow contents to generate the box. This will cover the “set inherited properties without generating a box” use case, won't have the all weirdness or any custom inheritance, and will (mostly) prevent authors from applying the *::contents {} (outside the display: contents case).

While the ::contents will be tremendously useful, I don't think having an extra display required for it will make it in any way worse to use.

@mayank99
Copy link

I agree with the "no custom inheritance or default all" part. It would be very confusing if this pseudo-element had special/unexpected behavior (also all can be bad for performance, since it is a shorthand for every single property).

I'm not sure if a new display value is the right move though. display is already ridiculously overloaded. What would even be the purpose of the "initial non-generating value" (if set in author-land)?

Another thought: If we're adding a new value to an existing property, should we do it for content, since that's what generates the ::before/::after pseudo-elements? This would make it consistent and probably easier to learn.

#a::contents { content: bikeshed; color: blue }
#b::contents { content: bikeshed; display: block }

Maybe this is a not good idea, since content: "some real value" would override/conflict with the element's own children. At the same time, it would be super useful for empty divs.

@Loirooriol
Copy link
Contributor Author

I agree content makes more sense than display. So I guess this option would be:

  • ::contents gets content: none in the UA origin.
  • Like ::marker, content: none and content: normal are different on ::contents.
  • The inheritance of an element is like:
    1. If there is no parent, the initial value
    2. If the ::contents of the parent has content: none, the style of the parent
    3. The style of the ::contents of the parent

This implies that only the inheritance of content is a slightly weird, but content: inherit is already broken on Blink.

The example above would be:

#a::contents { content: normal; color: blue }
#b::contents { content: normal; display: block }

No need to add a new content: bikeshed value. Values different than normal and none would presumably work like on normal elements, which IIRC was either behaving like normal or turning the (pseudo-)element into a replaced image.

@kizu
Copy link
Member

kizu commented Jun 11, 2024

I'm mostly ok with using the content: normal for this, the only small issue is that the ::contents { content: normal } will be very easy to do, but I think that could be ok.

@mayank99
Copy link

I'm also mostly ok with content: normal, though I wonder if there is some potential confusion, since it's the initial value and there is no box generated initially. It almost looks like a no-op declaration.

I also just noticed content: contents is a thing, which may be even more appropriate.

#a::contents { content: contents; color: blue }
#b::contents { content: contents; display: block }

@Loirooriol
Copy link
Contributor Author

I planned to use content: contents to explain ::contents. But IIRC, content: contents was introduced to address some problem that I don't remember, but then it became unnecessary and nobody plans to implement it, so the idea was to remove it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests