Properties vs. Attributes

January 25, 2025 · View on GitHub

When you are working with HTML, you can have DOM nodes like <div></div>. And you can add attributes to those DOM nodes, like <div class="user-info"></div>.

When you are creating DOM nodes in JavaScript, there are two ways to add attributes like this:

  1. Attributes — You can use the setAttribute function. So adding a class attribute would look like this: domNode.setAttribute('class', 'user-info').

  2. Properties — JavaScript often exposes an alternate way to set these attributes. Another way to add a class attribute would be like this: domNode.className = 'user-info'

Notice that the attribute is called class but the property is called className. This is because class is a reserved word in JavaScript. Point is, attributes and properties do not always match up.

It is actually a bit crazier than that though. Sometimes an attribute exists, but there is no corresponding property. For example, as of this writing the webkit-playsinline can be added with setAttribute, but there is no corresponding property. And with SVG, you cannot use properties at all, you must to use setAttributeNS for everything.

With all the corner cases here, it makes sense to have access to both approaches.

The functions in Html.Attributes are generally implemented using attributes (except for a few things that have to be properties). Using attributes have a couple of benefits for standard attributes:

  1. It’s possible to remove an attribute, but not always possible to remove a property. For example, the HTML <a> results in a link without any href. The specification defines that as a link placeholder, and a use case is a menu where the current page does not need to link to itself. But .href is actually set to the empty string ("") on the DOM node. The HTML <a href="/about"> results in a link with a href. If you have just <a>, you can either do .href = "/about" or .setAttribute("href", "/about") to set the href. But how do you remove the attribute again? If you try .href = "", that actually results in <a href=""> (which is not the same as the href attribute being missing), while .removeAttribute("href") does result in <a>.

  2. When initializing an Elm app, you give it a DOM node to render into, or the <body> node is used. What happens if the node to render into isn’t empty? Elm then virtualizes the DOM into virtual DOM. If the DOM nodes match what your app’s first render, the virtual DOM diffing won’t find any changes to make. This allows for server side rendering HTML, and then have Elm take over that content and avoiding lots of work at startup. Let’s take <a href="/about"> as an example again. When virtualizing, Elm has to make a guess: Did you use property "href" (Json.Encode.string "/about") or attribute "href" "/about" in your Elm code? If we guess wrong, the first render will do an unnecessary DOM update, and if we guess right the first render won’t do anything as expected. Another complication is that attributes and properties don’t always have the same name, as mentioned above. When virtualizing DOM nodes, it’s possible to loop over all attributes that are set, but it’s not possible to loop over properties. For <div class="user-info">, a loop over attributes would get that class attribute, but if Html.Attributes.class is implemented with a property, we would have to translate class into className with a lookup table. By primarily using attributes in Html.Attributes, we don’t need a lookup table (except for a couple of edge cases).

  3. Some properties are read only – if you try to assign them an error is thrown. The most notable example of this is trying to do .className = "my-class" on an SVG element. That throws an error, while .setAttribute("class", "my-class") works. In Elm, both Html msg and Svg msg are type aliases for the same virtual DOM node type, so you can mix functions from elm/html and elm/svg without getting type errors. Html.Attributes.class used to be implemented by setting the className property, and a common mistake was accidentally using using Html.Attributes.class on an SVG element, instead of Svg.Attributes.class (which is implemented by setting the class attribute), which would then cause hard to debug runtime errors. By setting the class attribute in Html.Attributes.class this issue is avoided.

  4. Attributes are easier to diff. If we take the example with the link again, if you do .href = "/about", then what is the value of .href afterwards? You might think it’s "/about", but it’s actually "https://example.com/about" (if the code is running on https://example.com). .href returns the full URL. If you set it to a non-full URL, the browser resolves it to the full URL. This is important because some properties change automatically through user interactions. For example, .value of a text input updates automatically as the user types into it. But if you have code like Html.input [ Html.Attributes.value "hardcoded" ] you would expect the input to always say “hardcoded”. If the virtual DOM diffing compares the old and new virtual DOM, it’ll see that both of them say that value should be "hardcoded" and decided that no update is needed. But in reality, the input displays whatever the user has typed. For this reason, it sounds like a good idea to instead diff the new virtual DOM against the actual DOM node when it comes to properties. The downside is the href example, though: If the virtual DOM says that href should be "/about" it will never be equal to the .href property on the DOM node, since it’s "https://example.com/about", which would result in us setting .href = "/about" on every render. Again, this is a reason to prefer attributes where possible. value is a good example of where a property is used on purpose: We need to set it on every render to make sure that it is set to the desired value.