webcomponents-cg icon indicating copy to clipboard operation
webcomponents-cg copied to clipboard

Complex selectors in ::slotted() and :host()?

Open Westbrook opened this issue 3 years ago • 20 comments

Now that all browsers support complex CSS selectors inside of :not(), what prevents the same within ::slotted() or :host()?

Browser announcements for reference:

  • https://webkit.org/blog/3615/css-selectors-inside-selectors-discover-matches-not-and-nth-child/
  • https://hacks.mozilla.org/2020/12/and-now-for-firefox-84/
  • https://blog.chromium.org/2020/12/chrome-88-digital-goods-lighting.html

Naively, this seems like the same internals would go into both resolutions, so much so that the idea of not just getting deep children, but resolving cascades up to next shadow root seem like they should just come for free now.

Example:

<parent-element>
    #shadow-root
        <div class="wrapper">
            <child-element>
                #shadow-root
                    <style>
                        ::host(.wrapper *) {
                            color: blue;
                        }
                        ::slotted(.wrapper *) {
                            color: red;
                        }
                        ::slotted(p a) {
                            color: green;
                        }
                    </style>
                    <p>This should be blue.</p>
                    <slot></slot>
                <p>This should be red...</p>
            </child-element>
            <child-element>
                #shadow-root
                    <style>
                        ::host(.wrapper *) {
                            color: blue;
                        }
                        ::slotted(.wrapper *) {
                            color: red;
                        }
                        ::slotted(p a) {
                            color: green;
                        }
                    </style>
                    <p>This should be blue.</p>
                    <slot></slot>
                <p>This should be red...</p>
            </child-element>
        </div>
        <child-element>
            #shadow-root
                <style>
                    ::host(.wrapper *) {
                        color: blue;
                    }
                    ::slotted(.wrapper *) {
                        color: red;
                    }
                    ::slotted(p a) {
                        color: green;
                    }
                </style>
                <p>This should NOT be blue.</p>
                <slot></slot>
            <p>This should NOT be red... but, <a href="#">This should be green</a></p>
        </child-element>
</parent-element>

Use cases: The ability to use :host(.wrapper *) aligns with the use of complex selectors on top of the idea of self references like :host([open]) etc. while allowing for the capabilities of :host-context() that never found footing outside of Blink, without the need for a completely new selector. This sort of functionality gives a component author the ability to outline specific realities about the delivery of their component is specific context. This is similar to how the details > summary:first-of-type element gets upgraded to have toggle UI: https://codepen.io/Westbrook/pen/QWGLdxP

This sort of .parent * resolving selector is interesting, and would seem like you'd want to apply it to content in a ::slotted() selector for all the same reasons, thought I do see how having the extended capabilities in the :host() selector outlined above would allow you to make this same ::slotted(.wrapper *) selection with :host(.wrapper *), in the case that we can't have it all. Certainly the more important addition of complex selecting in ::slotted() has to do with the ability to target styles on non-direct children.

A great power feature of an element with shadow DOM is its ability to encapsulate a world of both functionality and styles. Slotted content puts a really cool inside/out, outside/in twist on that. The ability to progressively enhance content with a custom element, turning vanilla content into vibrant content currently falls a little short when the encapsulated space can't really bring descendent content into the family without addressing it with styles from JS. While the content being slotted means we do have the means to style it from the outside, the benefit of slotting it into an element with shadow DOM should be that we're not required to. Imaging the child-element in the above example is an article or aside or callout that should take on a new background color when upgraded, without the ability to control the color of the anchor tag grandchild element it's possible that the content can become unaccessible without requiring advance knowledge of custom element usage (if a custom element and the :not(:defined) selector is even available) for a consuming developer to ensure that their content will be delivered appropriately.


It's possible that this issue belongs in https://github.com/WICG/webcomponents/issues, and I'm happy to move it if so. However, I wanted to start by asking this as an opportunity to learn about the possible reasoning behind the currently available APIs and gather community interest in the capabilities it unlocks before attempting to more formally position it as a proposal for standardization.

Westbrook avatar Jan 27 '21 23:01 Westbrook

there is a lot in this issue - maybe we can split it and only focus on one part at a time? 🤔

I personally would be most interested in ::slotted(.wrapper .some .sub-selector) and if we can push something in this direction - maybe we should make it a dedicated issue?

daKmoR avatar Feb 17 '21 07:02 daKmoR

@mfreed7 mentioned opening a bug to get the current state of possibility in Chrome. Do you have any thoughts on this, Mason? Maybe we could get that bug and this issue associated for cross-tracking purposes? OR if nothing else linked so those of us interested can star it?

Westbrook avatar Feb 18 '21 18:02 Westbrook

Took me a while to find it, but I opened this Chromium bug to discuss. See this comment in particular for the likely reason complex selectors won't be supported here.

mfreed7 avatar Apr 15 '21 16:04 mfreed7

@Westbrook thanks for the issue!

As @daKmoR, I also really interested in ::slotted(.wrapper .some .sub-selector).

Create a component library based on Web Components gets a bit tricky with a limitation like this. We need it for "simple" examples like these:

<x-card>
  <div slot="title">The title</div>
  <p>Text one with <a href="#">link</a>!</p>
  <p>Text two!</p>
</x-card>
<x-banner>
  <div class="bx--grid"> <!-- `bx--grid` is a class to position the content at application level -->
    An announcement with a <a href="#">link</a>!
  </div>
</x-banner>

abdonrd avatar Jul 21 '21 12:07 abdonrd

These use cases are great @abdonrd! We’re just now starting the process of developing a “Pain Points and Day Dreams” sort of report to present to the WC working group and hope you’ll support us in getting all the important issues clearly outlined to share therein.

Westbrook avatar Jul 21 '21 23:07 Westbrook

Complementing a little bit the last comment from @abdonrd and if this can be useful for you @Westbrook. Another common use case that Abdón and me faced before is the use of <span></span> to add not common symbols as it happens in a research environment for mathematics or physics (to add a more concise case).

<x-card>
  <div>
    Lorem ipsum <span>Symbol here</span> dolor sit ame
  </div>
</x-card>

And this can be extrapolable to images, for example. Or another element that needs to fit in a specific space/position inside a text.

Tansito avatar Jul 22 '21 10:07 Tansito

Allowing complex selectors inside ::slotted() would go a long way. Any assemblies that require a combination of nested elements would benefit from this.

<fancy-table>
  <table>
    <tr>
      <td>Style me differently after fancy-table is connected</td>
    </tr>
   </table>
</fancy-table>

A more realworld example of this might be providing responsive behaviors to tables a la Filament Group's TableSaw library. http://filamentgroup.github.io/tablesaw/demo/stack.html

davatron5000 avatar Jul 23 '21 18:07 davatron5000

+1 to this. A pretty good example of this is the Duet DS table https://www.duetds.com/components/table/

frostyweather avatar Jul 23 '21 18:07 frostyweather

Oh hey I built the duet table component 😄 I had to opt out of shadow dom when building that component, specifically because it is not possible to style slotted deep table elements like td etc

In short +1 to this!

WickyNilliams avatar Dec 21 '21 18:12 WickyNilliams

Stumbled on another use case today:

<button-group>
	<button>a</button>
	<button>b</button>
</button-group>

<button-group vertical>
	<button>a</button>
	<button>b</button>
</button-group>

You want to style the slotted buttons differently for horizontal and vertical button groups, which doesn't seem possible without this.

LeaVerou avatar Jul 09 '22 17:07 LeaVerou

Would

:host([vertical]) ::slotted(button)

Not work?

WickyNilliams avatar Jul 09 '22 17:07 WickyNilliams

I expected that would work too, but nope (at least in Chrome). It appears :host and :host() cannot do descendants, and neither can ::slotted().

LeaVerou avatar Jul 10 '22 10:07 LeaVerou

@LeaVerou check this out: https://codepen.io/Westbrook/pen/VwXjeoR

Is there something more complex that you're trying to do? If so, the full use case would be a great addition to this issue.

Westbrook avatar Jul 10 '22 12:07 Westbrook

As @Westbrook showed in this CodePen, I can't seem to select parts after selecting the slotted last of type of a custom element. https://codepen.io/Westbrook/pen/MWVbRax

eduardomecchia avatar Jul 15 '22 14:07 eduardomecchia

@LeaVerou check this out: codepen.io/Westbrook/pen/VwXjeoR

Is there something more complex that you're trying to do? If so, the full use case would be a great addition to this issue.

Well, this is embarrassing. I was tripped up by specificity like a newb. 😳 Did not expect that ::slotted() would not contribute anything to specificity and things like ::slotted(button:not(:first-of-type)) would be overridden by author button styles (demo), so I had assumed the selectors weren't working at all (and somehow missed them being applied in the dev tools, no idea how). So… yeah, nothing to see here, move along. 😛

Edit: Though that might be a Chrome bug, since per spec:

The specificity of ::slotted() is that of a pseudo-element, plus the specificity of its argument.

LeaVerou avatar Jul 15 '22 15:07 LeaVerou

Yeah it's a constant annoyance for me that the specificity is so low. It's the one instance where I regularly use !important to demarcate essential styles

WickyNilliams avatar Jul 15 '22 15:07 WickyNilliams

Yeah, I also ended up using !important 😕 But check out my edit above: could it be a bug or am I misreading the spec? Though all browsers seem to agree on 0 specificity, so even if it is a bug, it's more likely that we change the spec than browser behavior at this point. 😕

LeaVerou avatar Jul 15 '22 15:07 LeaVerou

I'm quite biased, but I feel that :host(:has(...)) pointing into the light DOM (in Safari, currently, the spec hasn't fully landed in Chromium): https://codepen.io/Westbrook/pen/PoRVbQo kind of/really opens the door for these sorts of capabilities, right? 😜

Westbrook avatar Aug 19 '22 15:08 Westbrook

I'm quite biased, but I feel that :host(:has(...)) pointing into the light DOM (in Safari, currently, the spec hasn't fully landed in Chromium): https://codepen.io/Westbrook/pen/PoRVbQo kind of/really opens the door for these sorts of capabilities, right? 😜

It certainly does @Westbrook. I was a bit shocked to see that Safari was alone with support here. All I need to do is change the padding of the host element when an SVG is slotted in any of the slots.

<foo-button>
   <svg></svg>
</foo-button>

<foo-button>
   <svg slot="start"></svg>
</foo-button>

<foo-button>
   <svg slot="end"></svg>
</foo-button>

:has() within the :host does the trick perfectly. Back to JS it seems 😢...

:host(:has(svg)) { 
    padding: var(--different-padding);
}`

chrisdholt avatar Mar 15 '23 01:03 chrisdholt

I'm quite biased, but I feel that :host(:has(...)) pointing into the light DOM (in Safari, currently, the spec hasn't fully landed in Chromium): https://codepen.io/Westbrook/pen/PoRVbQo kind of/really opens the door for these sorts of capabilities, right? 😜

I was chatting with @Westbrook about this a bit this morning since I recently stumbled upon a use case: it could be used to progressively enhance form controls like radio buttons.

Blog post Codepen

The sense I get from this discussion is that it’s already to spec, just not well supported. I’m not sure if this adds anything to the discussion, but any case, figured it couldn’t hurt to share here :)

Noleli avatar Dec 14 '23 16:12 Noleli