Styling Custom Elements Without Reflecting Attributes

I've been struggling with the idea of reflecting attributes in custom elements and when it's appropriate. I think I've identified a gap in the platform, but I'm not sure exactly how we should fill it.

I'll explain with an example. Let's say I want to make a simple badge component with primary, secondary, and tertiary variants.

<my-badge variant="primary">foo</my-badge>
<my-badge variant="secondary">bar</my-badge>
<my-badge variant="tertiary">baz</my-badge>

This is a simple component, but one that demonstrates the problem well. I want to style the badge based on the variant property, but sprouting attributes (which occurs as a result of reflecting a property back to an attribute) is largely considered a bad practice. A lot of web component libraries do it out of necessary to facilitate styling — including Shoelace — but is there a better way?

The problem #

I need to style the badge without relying on reflected attributes. This means I can't use :host([variant="..."]) because the attribute may or may not be set by the user. For example, if the component is rendered in a framework that sets properties instead of attributes, or if the property is set or changed programmatically, the attribute will be out of sync and my styles will be broken.

So how can I style the badge based its variants without reflection? Let's assume we have the following internals, which is all we really need for the badge.

<my-badge>
  #shadowRoot
    <slot></slot>
</my-badge>

What can we do about it? #

Filling the gap #

I'm not sure what the best solution is or could be, but one thing that comes to mind is a way to provide some kind of cross-root version of :has that works with :host. Something akin to:

:host(:has-in-shadow-root(.some-selector)) {
  /* maybe one day… */
}

If you have any thoughts on this one, hit me up on Twitter.