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:
- The
id
attribute reflects on all elements - The
<details>
element reflects theopen
attribute when expanded - The
<dialog>
element reflect theopen
attribute when shown - The
<button>
element reflects thedisabled
attribute when disabled - The
<img>
element reflects thesrc
andalt
attributes when set as properties - The
<video>
element reflects thesrc
attribute when set as a property - The
<input>
element reflects thetype
attribute when set as a property - The
<progress>
element reflects thevalue
attribute when set as a property - The
<form>
element reflects theaction
andmethod
attributes when set as properties - The
<textarea>
component reflects therows
andcols
attributes when set as properties
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:
- It represents a current state that the user can modify and/or may be interested in observing, such as
checked
,disabled
,open
,readonly
, etc. - It represents a value that affects how the component is styled internally or externally. This allows us to target variants without using an internal wrapper and/or replicating values internally.
- It makes the debugging experience more intuitive for authors and consumers.
An attribute should not reflect when:
- The data type is neither string, number, nor boolean.
- The value is content or otherwise too cumbersome to store in the DOM, e.g. the content of a text area.
- The user is unlikely to have interest in observing the attribute for state or styling purposes.
Notes:
- It's OK if an attribute reflects and a custom state is applied. In this case, the consumer can choose how to target it in their own styles.
*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.