All Articles

Adding attributes or properties to Web Component slots

On my journey as a Web Components author, I’ve often found myself wishing for the ability to pass attributes/properties to an element inserted via <slot />.

Unfortunately this is not something supported by the WC spec or the <slot /> element. However I’ve been able to implement a workaround that works relatively well.

The gist of the technique is to dynamically set the desired attributes or properties via a proxy component that wraps the <slot /> tag. Here is the code for that using StencilJS:

import { Component, Element, Watch, Prop, h } from '@stencil/core';

@Component({
  tag: 'attr-injector',
})
export class AttrInjector {
  @Element() el: HTMLElement;
  childEl: HTMLElement;

  @Prop() attrs: any = {};

  componentWillLoad() {
    this.childEl = this.el.firstElementChild as HTMLElement;
    this.injectAttrs();
  }

  @Watch('attrs')
  attrsUpdated() {
    this.injectAttrs();
  }

  injectAttrs() {
    for (let [key, value] of Object.entries(this.attrs)) {
      this.childEl.setAttribute(key, value as string);
    }
  }

  render() {
    return (
      <div class="attr-injector">
        <slot />
      </div>
    );
  }
}

Basically, what’s happening here is that we’re looping over the key/values passed via the attrs prop and using setAttribute to set them on the first child of the injector component. Now here is how you would use that in your component:

import { Component, Prop, h } from '@stencil/core';

@Component({
  tag: 'my-component',
})
export class MyComponent {
  @Prop() childClass: string;
  @Prop() value: string;

  render() {
    return (
      <div class="my-component">
        <attr-injector attrs={{
          class: this.childClass,
          value: this.value,
        }}>
          <slot />
        </attr-injector>
      </div>
    );
  }
}

Nothing new here, we’re just using the injector component we previously defined to pass the attributes we want to the slotted content. Finally, if you implemented your own component like this:

<my-component class="my-input" value="1">
  <input />
</my-component>

It would inject the attributes into the input component so that the final rendered DOM looked like this:

<my-component class="my-input" value="1">
  <div class="my-component">
    <attr-injector>
      <input class="my-input" value="1" />
    </attr-injector>
  </div>
</my-component>

That’s it!

N.B. The example above was using attributes but can easily be adopted for props instead by replacing this.childEl.setAttribute(key, value) with this.childEl[key] = value