skip to Main Content

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>

2

Answers


  1. 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 are trying are either not pseudo-classes or pseudo-classes that based on tree information.

    Fortunately, part names act similarly to classes: multiple elements can have the same part name, and a single element can have multiple part names. (From the same spec as well.)

    Hence, you can use multiple part names such as input input-1.

    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 input-1)  {
      background: #ff00ff;
    }
    
    web-component::part(input) {
      border: 2px solid orange;
    }
    <web-component size="6" id="shadow-dom-host"></web-component>
    Login or Signup to reply.
  2. Like 9 out of 10 developers your are falling into the DOM parsed trap,
    they experience no issues because they all define their Web Components (too) late.

    There is no DOM when the Web Component is defined before DOM is parsed:

    <script>
    customElements.define("web-component",
      class extends HTMLElement {
        constructor() {
          super();
          this.size = this.getAttribute('size') || console.error("No attributes!");
        }
      }
    );
    </script>
    
    <web-component size="6"></web-component>

    connectedCallback

    Only in the connectedCallback are the attributes available.

    Note! There is no .innerHTML lightDOM yet!
    Because the connectedCallback fires on the opening tag
    See my Dev.to blogpost

    But its save to attach shadowDOM here. And create DOM with a handy helper function createElement

    And you do not want to use appendChild inside a loop, because it causes unnecessary reflow/paints for every item.

    <script>
      customElements.define("web-component",
        class extends HTMLElement {
          connectedCallback() {
            const createElement = (tag, props = {}) => Object.assign(document.createElement(tag), props);
            this
              .attachShadow({mode:"open"})
              .append(
                createElement('style', {
                  innerHTML: 
                    ':host {white-space: nowrap}' +
                    'input {text-align:center;width: 3ch;height: 3ch}' +
                    'input:not(:last-child){margin-right:.5ch}'
                }),
                // spread [size=N] inputs
                ...(Array(~~(this.getAttribute("size") || 5)).fill(0)
                  .map((x, idx) => createElement('input', {
                    type: "text",
                    part: "input input-" + idx,
                    onclick: (evt) => console.log("clicked",idx)
                  })) //map
                ) //Array
              ) //append
          } //connectedCallback
        });
    </script>
    
    <style>
      web-component::part(input input-1) {
        background: #ff00ff;
      }
    
      web-component::part(input) {
        border: 2px solid orange;
      }
    </style>
    
    <web-component size="6"></web-component>
    <br>
    <web-component size="5"></web-component>
    <br>
    <web-component size="2"></web-component>

    Be aware

    The connectedCallback is called again when you move the Web Component in the DOM (append, drag-drop etc.)

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search