Reflection and Custom States in Web Components

In the Web Component world, attribute reflection is commonly used to style custom elements both internally and as public APIs for consumers. If you're not familiar, attribute reflection occurs when an attribute in the DOM is updated due to changes in a corresponding property.

A lot of folks conflate attributes and properties, so here's an example.

<!-- This is the open attribute -->
<details open>
  ...
</details>

<script>
  // This is the open property. Changing it will "reflect"
  // back and update the attribute - but not all attributes
  // reflect!
  document.querySelector('details').open = false;
</script>

The pattern of using reflection as a styling API likely emerged out of necessity, as the suggested alternative, Custom States, only recently landed decent browser support.

However, from an HTML purist's perspective, we shouldn't sprout attributes. It's largely considered an anti-pattern because attributes are intended to be an input mechanism that should only be set by the consumer.

This is a sensible argument, but many native elements don't follow this convention. Here are some examples:

Of course, not all properties reflect. In fact, the rule seems rather arbitrary. For fun, I asked my friendly neighborhood AI — who is more patient and arguably more skilled at deciphering long technical documents then I am — when properties should reflect back to attributes, per the HTML spec.

Reflected properties in HTML are properties that, when changed, update the HTML markup to ensure synchronization between the HTML and the DOM. This allows for changes to be preserved if the page is reloaded or serialized back into a string. The decision to reflect a property is determined by the HTML specification on a case-by-case basis, considering whether the property represents the initial state or current behavior of the element.

It seems there's not an obvious convention to follow. If we're reflecting properties back to attributes to represent an element's initial state, wouldn't those attributes already be set by the author of the HTML?

Besides, some elements reflect their current state, such as <dialog open>, <details open>, <input checked>, etc. This suggests it's OK to reflect attributes to represent state. But if that's what Custom States intends to solve, why didn't these native elements use a state, such as :open?*

So when should we reflect attributes? #

I usually prefer to align with the platform, but the platform isn't clear on this one. Because things like this bother me, I'm going to outline some rules I use to determine if an attribute should or shouldn't reflect.

An attribute should reflect when:

An attribute should not reflect when:

Notes:


*There is a non-zero chance that we are stuck in a perpetual hell for the sake of backwards compatibility. Alas, the lack of an obvious pattern makes it hard to identify a best practice moving forward, and user expectation may be contrary based on prior art.

**I suspect that some folks will disagree with this, so I'll elaborate. If I have a component that supports multiple variants, e.g. primary, secondary, tertiary, the user will typically set an attribute on the element: <my-badge variant="primary">. But what if they set the corresponding property instead because they're using a framework that sets properties instead of attributes? This means we can't rely on :host([variant="primary"]) in our styles, so we either have to a) map the selected variant to classes somewhere internally, possibly requiring a wrapper element that wouldn't be needed otherwise; b) use custom states…except we'd need to add one for every possible variant which isn't ergonomic for authors or end users; or c) use something like a data-variant attribute to…shit, this is the same as attribute reflection.

***Unfortunately, some libraries that use DOM morphing might break custom elements that reflect attributes. As such, it's a good idea to make sure your default styles don't rely on them. For example, if the default styles for <your-button variant="default"> get lost when you remove the variant attribute, you're gonna have a bad time.