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

[selectors] Pseudo-class to indicate when a slot has content #6867

Open
plinss opened this issue Dec 8, 2021 · 58 comments
Open

[selectors] Pseudo-class to indicate when a slot has content #6867

plinss opened this issue Dec 8, 2021 · 58 comments

Comments

@plinss
Copy link
Member

plinss commented Dec 8, 2021

I'm developing several web components and I'm finding a need to tell when a slot has content or not. (e.g. to apply margins or not so empty slots don't impact layout, but layout well when they do, etc.)

The :empty pseudo-class applies when the slot doesn't have direct content, even when it has slotted content, so doesn't work. Using slot:has(::slotted(*)) wouldn't detect text-only content.

Once possibility would be to redefine :empty to detect slotted content, but it may be useful to keep the current behavior to differentiate empty slots from slots with default content (and the change may break exiting content). So a new pseudo-class that only detects slotted content may be useful.

@WickyNilliams
Copy link

I have also needed this on several occasions. At the moment, you need to (at the minimum) inspect assignedNodes/assignedElements to determine this, and then add css classes to elements to modify styling. This is far from ideal for such a common use case.

A typical use case for me is to hide (display:none) a <slot> if it is has no assigned content. The reason being that i often set a slot's display to something other than contents e.g. making a slot a flex container so that i can use gap to space slotted children.

So being able to target a slot with assigned content is the most pressing use-case. Though I think it's also worth considering whether there should be separate pseudo-class for each of these states:

  • assigned content
  • default content
  • empty (i.e. no default or assigned content)

@MaxArt2501
Copy link

I'd like to point out that a :has-assigned-content pseudo-class (or better name) would be nice to have since:

  • developers would like to avoid using JavaScript for what's basically a declarative effect;
  • working around with :host(:not(:empty)) slot (for unnamed slots) or :host(:has([slot='foo'])) slot[name='foo']:
    • is verbose;
    • has content limitations (e.g. for components with multiple slots);
    • doesn't work for manual node assignment.

I'm quite keen myself to set a display value different than contents for slots, but this would also allow to style sibling or parent nodes (via :has()).

@castastrophe
Copy link

Perhaps inline with :has, :has-assigned (content could be a misnomer because it might contain text or nodes). This would enable styles such as:

slot[name='foo']:not(:has-assigned) {
   display: none;
}

A good partner to this spec would be #6620

@castastrophe
Copy link

This seems like a related request in the WICG space: WICG/webcomponents#936

@css-meeting-bot
Copy link
Member

The CSS Working Group just discussed [selectors] Pseudo class to indicated when a slot has content, and agreed to the following:

  • RESOLVED: Take this up for Selectors 5, fantasai and tabatkins will come back with proposed text
The full IRC log of that discussion <emeyer> fantasai: I wanted to know if this is something we want to pursue. plinss was asked whether or not we can check to see if something has slotted content. Is this something we want to do?
<emeyer> TabAtkins: The use case makes sense and the argument is reasonable. The fact you can’t tell if text content has been slotted in leaves a whole. I say was call it ‘:slottted’
<fantasai> s/slottted/has-slotted/
<emeyer> PaulG: Is the example intended to be a slot name?
<emeyer> TabAtkins: ‘::slotted’ will select anything slotted into that slot.
<emeyer> fantasai: Tab and I should draft a proposal.
<emeyer> Rossen: Would this be level 4?
<emeyer> fantasai: No, 5
<fantasai> ACTION: Tab + fantasai to draft into Selectors 5
<emeyer> RESOLVED: Take this up for Selectors 5, fantasai and tabatkins will come back with proposed text

@AutoSponge
Copy link

in the example the ::slotted(*), the * is intended to be a selector but it may have more benefit to developers if the slot name was also queryable in a similar manner. I'm thinking of cases where a component uses multiple sibling slots.

@tabatkins
Copy link
Member

Can you elaborate on what you're thinking of? The ::slotted() pseudo-element lives on a slot element, so you can already distinguish between multiple slots. The selector argument is matched against the elements put into the slot.


As I said in the minutes above, I think :has-slotted is a reasonably straightforward name - not too long, but clearly targeted at its use-case.

@Westbrook
Copy link

@tabatkins where are you seeing the :has-slotted selector being available? Feels like the optimal usability of it would benefit from it being available all the way up to :host:has-slotted([slot=name]).

Would be we able to do something here along the lines of .assignedElements({flatten: true}) to ensure this selector is pointing to "content" and not just "possible content" (e.g. <slot> elements)?

Is there any benefit in being forward looking to the discussion in #7922 and make it a combinator? Maybe that doesn't make sense in this case...

@castastrophe
Copy link

castastrophe commented Feb 6, 2023

An interesting edge case to consider will be slots with default content as well. Does default content count as has-slotted? i.e.,

<slot>
    <p>Lorem ipsum dolor...</p>
</slot>

@MaxArt2501
Copy link

An interesting edge case to consider will be slots with default content as well. Does default content count as has-slotted? i.e.,

<slot>
    <p>Lorem ipsum dolor...</p>
</slot>

I'd say no, because that <p> isn't technically slotted. One can combine :has-slotted with :empty, if necessary.

But perhaps there may be more use cases.

@Westbrook
Copy link

Great nuance in:

<slot>
   <p>Lorem ipsum dolor...</p>
</slot>

That p comes back in .assignedElements({flatten: true}) but not .assignedElements() and is NOT available in ::slotted(p) but is in :host p, which vaguely implies that we'd need two selectors or rather one with some settings (!? 😳, not sure if there are examples of that beyond attribute selectors) to ensure we have full control over the content we're shipping here.

I'm not quite sure how you could combine :has-slotted and :empty to get the sort of outcomes desired here, specifically in that :empty is an element state selector and :has-slotted is a child state selector, they don't apply to the same elements in my imagination. Could you share more on that? I could maybe see :not(:has-slotted(...)) getting you somewhere (assuming the p wasn't queried by this selector), but then how would you capture that the p is there, unless we can also have :host:has (point of order: not in the function reference, but directly on the element) that pointed into the shadow root...interesting.

@MaxArt2501
Copy link

Now that I think of it, it's not just :empty that could come in handy, but also :has(). If I want to hide a slot that has no slotted node and no default content, I could achieve it with

slot:not(:has(*)):not(:has-slotted(*)) {
  display: none;
}

(:empty could be useful when there's just default text as content, like <slot>Hello</slot>... alas, it's brittle enough that it'd fail with just some whitespace.) :has and :has-slotted should cover all the basic uses. For example, if we have something like this, where that content can be wither passed as an attribute (for simple text) or as slotted content (for HTML content):

<slot name="title">${this.cardTitle}</slot>

I wasn't aware that default slot content is returned by .assignedElements({flatten: true}), as considering it "assigned" to any slot seems counter-intuitive to me. All in all, I'm not sure that trying to mimic the behavior of a JS method would make sense here.

@tabatkins
Copy link
Member

where are you seeing the :has-slotted selector being available? Feels like the optimal usability of it would benefit from it being available all the way up to :host:has-slotted([slot=name]).

It's on the slot, indicating that that particular slot element has slotted content in it. If you want to style the host based on whether any of its slots have slotted content, you can use :has(slot:has-slotted) to check. ^_^

An interesting edge case to consider will be slots with default content as well. Does default content count as has-slotted?

I'd say no. It's not slotted content, and in particular, it's not useful to detect slotted content in this case if this is true. That is, if :has-slotted matches a slot with default contents, then what would one do with the selector? It becomes guaranteed to match, and at that point you can just put a class on the slot and use that instead. On the other hand, having it not match when the slot is using its default content lets you make a meaningful distinction that you can only know at runtime, which is useful.

@justinfagnani
Copy link

justinfagnani commented Jun 20, 2023

We have a number of SSR users who really need this selector.

There a few reasons this is critical in some SSR implementations:

  1. They may not actually use a DOM on the server - they're just emitting strings to the HTTP response. So there may be no way to determine if an element slots into a parent's slot.
  2. The current workaround of listening for slotchange and setting a class uses imperative DOM code that doesn't work on the server, and doesn't work on initial hydration without JS.

It seems like the idea of a new selector is fairly well received... can this idea be added to an upcoming csswg meeting?

@claviska
Copy link

In the case of Shoelace, we're using imperative DOM code to do slot detection which, as @justinfagnani mentioned, doesn't work on the server. We can probably work around some of these checks by changing structure/display so empty slots collapse, but this is very uncomfortable and won't solve all of our use cases.

As it stands, without such a selector, we can't get SSR fully functional. This has, unfortunately, been a pretty big hurdle for adopters who want to use our custom elements with metaframeworks + SSR.

@michaelwarren1106
Copy link

I'm using the same imperative code mechanism as Shoelace above to detect slots having content. A css selector would be amazing.

@sorvell
Copy link

sorvell commented Oct 5, 2024

Tested :has-slotted in Chrome Canary with the "experimental web platform features" setting on.

The behavior is good for simple cases but not useful for nested composition. Perhaps this can be solved via #10771.

Content Expected Actual :has-slotted works ::slotted(*) assignedNodes() assignedNodes({flatten: true})
<glow-stuff></glow-stuff> no glow no glow yes no match [] []
<glow-stuff><input></glow-stuff> glow glow yes match [<input>] [<input>]
<template shadowRootMode="open"><glow-stuff><slot></slot></glow-stuff></template> no glow glow no no match [<slot>] []
<template shadowRootMode="open"><glow-stuff><slot></slot></glow-stuff></template><input> glow glow by luck match [<slot>] [<input>]
<template shadowRootMode="open"><glow-stuff><slot><input></slot></glow-stuff></template> glow glow by luck no match [<slot>] [<input>]

@keithamus
Copy link
Member

I'm going to Agenda+ as I'd like to capture a resolution that the current behaviour of not matching the flat tree is okay, and perhaps to resolve to investigating a way to match the flat tree (though perhaps this is :has-slotted() (#7922) or a /slotted/ combinator (#7922).

If web developers could please provide signal that the current behaviour as prototyped in Firefox and Chrome (that :has-slotted matches the non-flattened tree) is acceptable, that would help.

@Westbrook
Copy link

I really like having anything available in this area (is there any way that I can further explain the high level of important the web component developing community places on this API?). I am super excited about the implementation currently available in Chrome/Firefox (so many things we could never do have already been opened up to us with this API). However, not being able to select against the flattened tree will be a hard blocker on this API; maybe not today, maybe not tomorrow, but it will come up (much like questions around white space being "slotted") that would be better to be answered today, if only directionally.

Personally, I can certainly be appeased with the selector matching .assignedNodes({flatten: true}) only. For my experience, the ability to build complex, deeply slotted applications is and will continue to be greatly constrained by not supporting this. This also aligns it more directly with the behavior of ::slotted(). However, ::slotted() does present a questionable result in this example:

<div><template shadowrootmode="open">
  <div>
    <template shadowrootmode="open">
      <style>
        ::slotted(input) {
          color: red;
        }
      </style>
      <slot></slot>
    </template>
    <slot><input value="not styled" /></slot>
  </div>
</template></div>

Practically, we do need to support the spread.

Questions:

Does that mean multiple selectors? :has-slotted and :has-slotted-flatten could be a path, though it's interesting to need to go between the two.

Does flatten belong in the functional version? Always? Or as an option? :has-slotted(.selector !flatten) could be a path, but how this would apply to slotted text content is unclear. Also, does this mean each selector in the method get this option? :has-slotted(.selector !flatten, .other-selector), see .other-selector not feature flatten. Or would this need to be general to a single :has-slotted() usage?

Functionally, will component authors be doomed/required to "know" the slotted content depth?

<div>
  <template shadowrootmode="open">
    <div>
      <template shadowrootmode="open">
        <div>
          <template shadowrootmode="open">
            <style>
               p {
                 color: red;
               }
               slot:has-slotted(slot:has-slotted(slot:has-slotted(p))) + p {
                 color: green;
              }
            </style>
            <slot></slot>
            <p>Styled content</p>
          </template>
          <slot></slot>
        </div>
      </template>
    </div>
  </template>
  <p>Slotted content.</p>
</div>

Does a combinator approach mean that /slotted/ and /slotted-flatten/ could be made possible together, leveraged with better x-browser support for :host:has(...), and clarify both this and other situations herein? Can the combinator select on text?

Could any of these practically address selecting when content is delivered via the hole in ::slotted() outlined above?


Many thanks to all who are investing time into making this a high-quality and powerful additions to the web platform! 🙇🏼‍♂️

@sorvell
Copy link

sorvell commented Nov 4, 2024

@keithamus

I'm going to Agenda+ as I'd like to capture a resolution that the current behaviour of not matching the flat tree is okay

Based on the 3rd row of the table here, it's my opinion that:

The bare form of :has-slotted should match against the same tree (text node inclusive) as ::slotted(...).

Note, it's slightly confusing to call this the flattened tree because assignedNodes({flatten: true}) contains fallback content but ::slotted does not match it.

The basic use case for this:

  1. I have a button that needs different styling based on the presence of an icon. I will use :has-slotted to achieve this.
  2. If I want to allow a user of my composite element to take advantage of this feature, I'd do: <my-button><slot name="icon"></slot></my-button>.
  3. We need to match the "flattened"/::slotted tree to support this.

There are 2 separate issues which I think should be considered orthogonal to :has-slotted:

  1. matching out of scope distributed fallback content: there's currently no way to do this with ::slotted either so this shouldn't be uniquely handled by has-slotted.
  2. matching against whitespace only: there's also an issue with this using :empty so similarly we shouldn't uniquely handle it here.

@css-meeting-bot
Copy link
Member

The CSS Working Group just discussed [selectors] Pseudo-class to indicate when a slot has content, and agreed to the following:

  • RESOLVED: `:has-slotted` should match when the fallback content is not being displayed
The full IRC log of that discussion <kbabbitt> keithamus0: tldr from thread is chrome has :slotted, it can match on a few different things
<kbabbitt> keithamus0: concept of flattened vs non-flattened tree
<kbabbitt> ... flagged in firefox and chrome as non-flattened tree
<kbabbitt> ... so it can match direct descendant of a slot
<kbabbitt> ... some contention around this
<kbabbitt> ... setting flattened to true would mean it would make child slots transparent effectively
<kbabbitt> ... so if you have a slot slotted by another slot then that slot [missed]
<kbabbitt> ... my opinion is we should keep behavior as-is
<kbabbitt> ... that's the only way you can match slotted content
<kbabbitt> ... even if it's just a text node as a descendant of a slot element
<kbabbitt> ... web components cg is asking for commitment that this is acceptable
<emilio> q+
<kbabbitt> ... as long as we define the alternatives to that
<astearns> ack rachelandrew
<kbabbitt> ... chief use cases are: has my slot been populated? what has it been populated with?
<astearns> ack emilio
<kbabbitt> emilio: flattened true and flattened false - only effective difference is nested slots right?
<kbabbitt> keithamus0: that's true, has-slotted will always match populated text nodes
<kbabbitt> ... nested slots become transparent
<kbabbitt> [missed some context]
<kbabbitt> keithamus0: how do you determine nested slots is the short summary
<kbabbitt> emilio: to me, flattened tree behavior is what I'd expect as an author
<kbabbitt> ... components that use nested slots... that becomes useless
<kbabbitt> ... a bit confused about when you'd want to differentiate between useful slotted content and empty slot
<kbabbitt> ... why would you want flattened behavior?
<kbabbitt> keithamus0: other way around makes more sense for that use case, am I displaying default slotted content
<kbabbitt> emilio: right but if you have an empty slot you could do that?
<kbabbitt> keithamus0: I don't think so?
<kbabbitt> ... will have to test
<kbabbitt> emilio: my understanding is that you'd display default content of nested slot
<dbaron> (that would be my assumption ^)
<kbabbitt> ... flattened behavior is easier but I'm surprised that's most useful
<kbabbitt> ... would not object but might be worth checking
<kbabbitt> ...ideally has-slotted should reflect fallback content
<kbabbitt> keithamus0: agree, that's my understanding of what flattened false would do
<kbabbitt> emilio: assuming that's the case flattened false seems reasonable
<kbabbitt> ... if you want we can resolve that we make has-slotted match fallback content
<keithamus0> PROPOSED RESOLUTION: `:has-slotted` should match when the fallback content is not being displayed
<kbabbitt> astearns: doesn't mention flat tree deliberately
<kbabbitt> ... but if it turns out flat tree is desired behavior, [?]
<kbabbitt> keithamus0: need to confirm
<kbabbitt> ... whether a slot would display fallback content
<kbabbitt> ... but if that's the case it shouldn't use the flat tree, but if it isn't then it should
<kbabbitt> RESOLVED: `:has-slotted` should match when the fallback content is not being displayed

@keithamus
Copy link
Member

keithamus commented Jan 9, 2025

A quick test tells me the current functionality is correct; slotting a <slot></slot> element does not render fallback content, meaning flattened: false will observe this, while flattened: true does not.

Test case:

data:text/html,<div><template shadowrootmode="open"><div><template shadowrootmode="open"><slot>Nested Fallback</slot></template><slot></slot></div></template></div>

Therefore the existing WPTs and existing spec prose are sufficient.

@Westbrook
Copy link

I like that there is a resolution here, being able to select on this content is going to be a massive improvement for shadow root users at large. Many thanks for trudging through this! 👏🏼 👏🏼 👏🏼


However, there's one last thing that I think is highly important to consider with this feature...

The fact that many developers have difficulty grasping the nuances of styling shadow DOM and leveraging ::slotted(...) to begin with should point us away from using the language "slotted" in this feature. The resolution above for :has-slotted not to select against content that can be addressed with ::slotted(...) today means it will be just as confusing as it is powerful when shipped as is. I can tell you as a person who has trained more people to work with shadow DOM than I can count over the last 10+ years that if we make the "slotted" in ::slotted(...) mean something different than the "slotted" in :has-slotted it will make learning these APIs even more difficult.

Example

Beyond the simple inability to address text nodes with ::slotted(...), there are other cases where "fallback content is not being displayed" but content isn't "slotted". If you have the following custom elements:

<my-button>
  <template shadowrootmode="open">
    <slot name=icon>Fallback content</slot>
    <slot></slot>
  </template> 
</my-button>

And it is leveraged within a larger pattern as follows:

<larger-pattern>
  <template shadowrootmode="open">
    <my-button>
      <slot name=icon slot=icon></slot>
      This is a button
    </my-button>
  </template>
</larger-pattern>

The <my-button> within the <larger-pattern> will not display the "Fallback content" even though it does not technically have anything "slotted" within it as seen by not being able to address ::slotted(slot) within the styles of the <my-button>.

Note: while "slotted text" is central to the OP here, knowing when an actual thing is slotted is a much wider pain that becomes harder to solve if we step on the language that should be used to do that work.

Alternatives

  • Continue to rhyme with :has(...) and do something like :has-assigned, which opens the door for :has-assigned-elements and :has-assigned-nodes, etc., etc.
  • Clarify the difference between this and other features by using something like :fallback-displayed so we can achieve the OPs goals with slot:not(:fallback-displayed) while staying more true to the newly resolved functionality of this feature.
  • Attempt to correct some of the shortcoming in :empty by introducing :text-node/:empty-text-node-only-like selector that allows us to do things like .el:is(:empty, :not(:empty-text-node-only)), slot:has-slotted(:text-node) and slot:has-slotted(:empty-text-node-only) et al.
  • Focus on :has-slotted rhyming functionally with ::slotted(...) by default and return to understanding the presence of text node in the future.

@sorvell
Copy link

sorvell commented Jan 9, 2025

RESOLVED: :has-slotted should match when the fallback content is not being displayed

Unless I'm mistaken, :has-slotted enables this, which is great:

<padded-with-icon>
  not padded
</padded-with-icon>
<padded-with-icon>
  padded <i-con slot="icon"></i-con>
</padded-with-icon>

But this has-a composition won't work because the icon slot inside padded-with-icon will not be showing fallback content. This seems pretty unfortunate.

<has-a-padded-with-icon>
  <template shadowrootmode="open">
    <padded-with-icon>
      <slot></slot>
      <slot name="icon" slot="icon"></slot>
    </padded-with-icon>
  </template>
  oops, this is padded
<has-a-padded-with-icon>

@sorvell
Copy link

sorvell commented Jan 10, 2025

RESOLVED: :has-slotted should match when the fallback content is not being displayed

This resolution does not fit with the general consensus in this issue that :has-slotted should behave generally consistent with ::slotted(), modulo text nodes.

The need here is to know if renderable content has been supplied to the slot. Unfortunately, this is not the same as if it will show fallback content. This is because a <slot> does not render but does prevent fallback content.

The ability to select if fallback content is showing, if specific elements have been slotted, or if slots themselves have been slotted should be handled as follow-on cases since they are (1) less needed and more complex, and (2) could be covered by a functional form and/or a combinator if/when that is possible.

Proposal

::slotted() matches the flattened tree so :has-slotted should as well. :has-slotted should match when assignedNodes({flatten: true}).length > 0. This is the same as when slotted(*)` would match except in the case of the default slot which can assign text nodes.

@justinfagnani
Copy link

I strongly agree with @sorvell here. I think it'll be confusing to have this divergence from ::slotted() and the composition use case is important.

@css-meeting-bot
Copy link
Member

The CSS Working Group just discussed [selectors] Pseudo-class to indicate when a slot has content, and agreed to the following:

  • RESOLVED: :has-slotted should use the flattened tree to resolve if content is slotted or not. We could later discuss a different pseudo-class for the other behavior.
The full IRC log of that discussion <bramus> keithamus: last week we resolved to match when the fallback content is not being displayed
<bramus> … whoich means using non flat tree
<bramus> … think this is a mistake
<bramus> … some people form the WC community reached out
<lea> q?
<lea> q+
<bramus> … was maybe overindexing on maybe wehter the fallback tree is displayed or <missed>
<bramus> … would like to change the resolution to use the flattened tree
<astearns> ack lea
<bramus> lea: do we stay consistent with JS API or do we do a better thing? Both are valid. Proably should follow JS appraoch for consistency
<bramus> … am wondering if we can have two pseudo classes
<bramus> keithamus: JS api offer the optionality through a flattened flag
<bramus> … has more to do with compat with ::slotted
<bramus> … cannot do slotted-slot
<bramus> … also composition
<bramus> … authors of components are more likely to want to know if th ething has been slotted.
<keithamus> s/fallback tree is displayed or <missed>/fallback content is displayed vs the flattened slots/
<bramus> … if that is many layers deep seems irrelevant to them
<bramus> lea: agree
<bramus> keithamus: proposed resolution is to use the flattened tree
<bramus> … to create new selector that uses non-flat tree I have no opinion
<lea> q?
<bramus> … polyfills: wont affect us because JS has optionality for both
<bramus> lea: I understand the sentiment that we need to ship this ASAP. really needed
<bramus> keithamus: I have i2s for chrome and I believe we are working towards one for firefox as well
<lea> s/I understand the sentiment /I completely agree with the sentiment /
<bramus> … need to resolve on this
<keithamus> PROPOSED RESOLUTION: :has-slotted should use the flattened tree to resolve if content is slotted or not.
<lea> +1
<bramus> astearns: with a future issue on adding a param or new pseudo to not use the flat tree
<bramus> … that’s a different, later, discussion
<bramus> keithamus: sgtm
<bramus> chrishtr: do we need resived resolution?
<bramus> astearns: no, wanted to point out other issue is out of scope
<lea> PROPOSED RESOLUTION: :has-slotted should use the flattened tree to resolve if content is slotted or not. We could later discuss a different pseudo-class for the other behavior.
<bramus> chrishtr: so someone should raise a new issue
<bramus> keithamus: i'll try and do that tomorrow
<keithamus> RESOLVED: :has-slotted should use the flattened tree to resolve if content is slotted or not. We could later discuss a different pseudo-class for the other behavior.
<bramus> keithamus: thanks everyone for your time

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: Friday afternoon
Development

No branches or pull requests