Using class vs host

Hello,

This is not really a bug, but rather a question about the design decision.

When you are using a component, let’s say s-badge, properties are reflected in the underlying shadow dom. For instance, the tone attribute is added as a class:

The code is minified so it’s hard to see, but I suppose that internally you are using an attributeChangedCallback to monitor properties, and generate the class, so something along this:

this.attachShadow({ mode: 'open' });

this.shadowRoot.innerHTML = `
      <div class="badge tone-${this.getAttribute('tone')}"><slot></slot></div>
    `;

Then, a CSS is generated with a constructable stylesheet:

As all Polaris components have their own shadow root and are completely scoped, I wonder if a better approach would not be to take advantage of the host selector, so instead you would just generate this:

this.shadowRoot.innerHTML = `
      <div class="badge"><slot></slot></div>
    `;

And, in your CSS, use the :host selector like this:

.badge {
  // Default style

  :host([tone="warning"]) & {
    // Override for the warning
  }
}

The main benefit being that you no longer need to monitor attribute for the sole purpose of styling. Of course, some properties might be more complex and would requires the attributeChangedCallback, but for ismple use case it would reduce the lines of code.

I’m actually also trying to create a similar web components based approach for an app, and found this host approach quite powerful. I wonder if this is something that has been considered and, if not, what were the issues you found with it?

Hey @bakura10 the main reason we didn’t put styles on the :host is that we no longer control them. With the components being shipped to a CDN and our need to ship updates confidently without breaking applications it means that we cannot without extreme added risk move styles to that selector.

With styles on the :host anyone can write s-badge { background: red } and it will override the badge style. This isn’t great for keeping consistent Admin design but it gets way more risky if folks start overriding layout and size of the elements.

That’s incorrect @alex-page . I was not referring about setting the style on the component itself. But just use the host to avoid having to reflect the properties and use attributesChangedCallback to re-render the component.

For instance instead of:

.badge {
  background: grey;
}

.badge.tone-warning {
  background: orange;
}

You would use:

.badge {
  background: grey;
}

:host([tone="warning"]) .badge {
   background: orange;
}

Or, if you’re ok with Safari 16, use nesting selectors:

.badge {
  background: grey;

  :host([tone="warning"]) & {
     background: orange;
  }
}

The style is still applied in the inner component and not on the host itself, so it is as safe as the current approach, expect that you don’t have to reflect each properties as a class and don’t have to use attributeChangedCallback for simple cases.

Some early component prototypes had this type of selector, but we ultimately chose not to do it because ultimately we decided that attributes are for configuration and that

It also makes sense that validation applies when getting/setting properties, but never when getting/setting attributes.

So if someone were to set an attribute to an invalid value, it would be a little more complex for us to apply the correct default styles while ensuring that value validation happens, e.g. for things like enum values that have a default fallback.

Also it’s slightly annoying to have to mess with case insensitivity for the attribute selector as well.

And finally, some changes to attributes/properties require more work than just pure styles; they could also require changes to what’s rendered.

So ultimately we chose this path because it was simpler to handle edge cases and it provided one single way of applying styles.

Thanks though! I love seeing suggestions!

Hi @Anthony_Frehner ,

Those are great points and seem all valid to me, thanks for clarifying!

1 Like