Search code examples
csscss-selectorsweb-componentshadow-dom

How to target the Nth element in a webcomponent shadowDOM with CSS ::part()


My <web-component></webcomponent> is very simply built, a series of <input type="text part="input" /> directly in the #shadow-root.

I can style them all at one without any troubles with:

web-component::part(input) {
  border: 1px solid #000
}

But neither of these works if I want to target a specific input:

web-component::part(input):nth-child(3) {}
web-component::part(input):nth-of-type(3) {}
web-component::part(input).input-3 {} /* with class properly defined in the web component */
web-component::part(input.input-3) {}
web-component::part(input:nth-of-type(3)) {}
...

Can't find any documentation about this.

EDIT : I forgot to mention that the inputs are not slotted, they're generated dynamically.

customElements.define("web-component", 
        class extends HTMLElement {
            constructor() {
                super();
                this.attachShadow({mode: "open"});
                this.size = this.getAttribute('size');
                this.template = document.createElement("template");
                this.template.innerHTML = '<style>'
                +':host {white-space: nowrap}'
                +'input {text-align:center;width: 3ch;height: 3ch}'
                +'input:not(:last-child){margin-right:.5ch}'
                +'</style>'
                this.render();
            }

            render() {
            
                this.shadowRoot.appendChild(this.template.content.cloneNode(true));

                for (let i = 0; i < this.size; i++) {
                    const input = document.createElement('input');
                    input.setAttribute('part','input');
                    input.classList.add('input-'+(i+1));
                    input.type = "text";
                    this.shadowRoot.appendChild(input);
                }
            }
        }
    );
web-component::part(input) {
  border: 2px solid orange;
}
web-component::part(input):nth-child(1) {
  background: #ff00ff;
}
web-component::part(input):nth-of-type(2) {
  background: #ff0000;
}
web-component::part(input).input-3 {
  background: #00ff00;
}
web-component::part(input.input-4) {
  background: #00ffff;
}
web-component::part(input:nth-of-type(5)) {
  background: #ff00ff;
}
web-component::part(input:nth-chlid(6)) {
  background: #ff0000;
}
<web-component size="6"></web-component>


Solution

  • It can't be done this way. From W3C css-shadow-parts spec:

    The ::part() pseudo-element can take additional pseudo-classes after it, such as x-button::part(label):hover, but never matches the structural pseudo-classes or any other pseudo-classes that match based on tree information rather than local element information.

    All selectors you're using won't be matched because they are either not pseudo-classes or pseudo-classes that based on tree information. One big reason for this is to allow the element creator to fully control what parts are exposed to be styled outside.

    Fortunately, part names act similarly to classes: multiple elements can have the same part name, and a single element can have multiple part names.

    Hence, you can use multiple part names such as part(input input-1). These can still be combined with non-structural pseudo-classes such as :focus or :hover.

    customElements.define("web-component", 
            class extends HTMLElement {
                constructor() {
                    super();
                    this.attachShadow({mode: "open"});
                    this.size = this.getAttribute('size');
                    this.template = document.createElement("template");
                    this.template.innerHTML = '<style>'
                    +':host {white-space: nowrap}'
                    +'input {text-align:center;width: 3ch;height: 3ch}'
                    +'input:not(:last-child){margin-right:.5ch}'
                    +'</style>'
                    this.render();
                }
    
                render() {
                
                    this.shadowRoot.appendChild(this.template.content.cloneNode(true));
    
                    for (let i = 0; i < this.size; i++) {
                        const input = document.createElement('input');
                        input.setAttribute('part','input input-'+(i+1));
                        input.type = "text";
                        this.shadowRoot.appendChild(input);
                    }
                }
            }
        );
    web-component::part(input) {
      border: 2px solid orange;
    }
    
    web-component::part(input input-1)  {
      background: #ff00ff;
    }
    
    web-component::part(input input-2):hover{
      background:green;
    }
    
    web-component::part(input input-3):focus{
      background:blue;
    }
    <web-component size="6" id="shadow-dom-host"></web-component>