A cartoon angle bracket figure gladly sharing his opinions to a crowd of symbols.

The attribute/property duality

Joeri Sebrechts

Web components, a.k.a. custom elements, are HTML elements that participate in HTML markup. As such they can have attributes:

<my-hello value="world"></my-hello>

But, they are also JavaScript objects, and as such they can have object properties.

let myHello = document.querySelector('my-hello'); myHello.value = 'foo';

And here's the tricky part about that: these are not the same thing! In fact, custom element attributes and properties by default have zero relationship between them, even when they share a name. Here's a live proof of this fact:

Now, to be fair, we can get at the attribute value just fine from JavaScript:

But what if we would also like it to have a value property? What should the relationship between attribute and property be like?

In framework-based code, we typically don't get a say in these things. Frameworks generally like to pretend that attributes and properties are the same thing, and they automatically create code to make sure this is the case. In vanilla custom elements however, not only do we get to decide these things, we must decide them.

Going native

Seasoned developers will intuitively grasp what the sensible relationship between attributes and properties should be. This is because built-in HTML elements all implement similar kinds of relationships between their attributes and their properties. To explore that in depth, I recommend reading Making Web Component properties behave closer to the platform. Without fully restating that article, here's a quick recap:

An easy way to get much of this behavior is to make a property wrap around an attribute:

Notice how updating the property will update the attribute in the HTML representation, and how the property's assigned value is coerced into the attribute's string type. Attributes are always strings.

Into the weeds

Up to this point, things are looking straightforward. But this is web development, things are never as straightforward as they seem. For instance, what boolean attribute value should make a corresponding boolean property become true? The surprising but standard behavior on built-in elements is that any attribute value will be interpreted as true, and only the absence of the attribute will be interpreted as false.

Time for another iteration of our element:

Which leaves us with the last bit of tricky trivia: it's possible for the custom element's class to be instantiated and attached to the element after the property is assigned. In that case the property's setter is never called, and the attribute is not updated.

This can be avoided by reassigning any previously set properties when the element is connected:

In conclusion

If that seems like a lot of work to do a very simple thing, that is because it is. The good news is: we don't have to always do this work.

When we're using web components as framework components in a codebase that we control, we don't have to follow any of these unwritten rules and can keep the web component code as simple as we like. However, when using web components as custom elements to be used in HTML markup then we do well to follow these best practices to avoid surprises, especially when making web components that may be used by others. YMMV.

In the next article, I'll be looking into custom elements that accept input, and how that adds twists to the plot.