Characteristics and properties are essentially different things. You can set properties and properties with the same name to different values. For example:

<div foo="bar">…</div>
<script>
  const div = document.querySelector('div[foo=bar]');

  console.log(div.getAttribute('foo')); // 'bar'
  console.log(div.foo); // undefined

  div.foo = 'hello world';

  console.log(div.getAttribute('foo')); // 'bar'
  console.log(div.foo); // 'hello world'
</script>

Fewer and fewer developers seem to know this, thanks in part to frameworks:

<input className="…" type="…" aria-label="…" value="…" />

If you were doing the above in a framework’s template language, you would be using attribute-like syntax, but under the hood it would sometimes set properties rather than attributes, and when to do this would vary from framework to framework. In some cases it sets features and properties as a side effect, but that’s not the framework’s fault.

Most of the time, these distinctions don’t matter. I think it’s a good thing that developers can have long and happy careers without caring about the difference between features and properties. However, it helps if you need to drill down to the lower levels of the DOM. Even if you think you understand the difference, maybe I’ll cover some details you haven’t considered yet. So let’s take a closer look…

Key differences

Before we dive into the fun stuff, let’s address some technical differences:

HTML serialization

Attributes are serialized to HTML, while properties are not:

const div = document.createElement('div');

div.setAttribute('foo', 'bar');
div.hello = 'world';

console.log(div.outerHTML); // '<div foo="bar"></div>'

Therefore, when you view the Elements panel in the browser developer tools, you will only see the attributes on the element, not the properties.

value type

To work in serialized format, attribute values ​​are always strings, while properties can be of any type:

const div = document.createElement('div');
const obj = { foo: 'bar' };

div.setAttribute('foo', obj);
console.log(typeof div.getAttribute('foo')); // 'string'
console.log(div.getAttribute('foo')); // '[object Object]'

div.hello = obj;
console.log(typeof div.hello); // 'object'
console.log(div.hello); // { foo: 'bar' }

Case sensitivity

Property names are not case-sensitive, whereas attribute names are case-sensitive.

<div id="test" HeLlO="world"></div>
<script>
  const div = document.querySelector('#test');

  console.log(div.getAttributeNames()); // ['id', 'hello']

  div.setAttribute('FOO', 'bar');
  console.log(div.getAttributeNames()); // ['id', 'hello', 'foo']

  div.TeSt = 'value';
  console.log(div.TeSt); // 'value'
  console.log(div.test); // undefined
</script>

However, property values ​​are case-sensitive.

Well, where things start to get blurry is:

reflection

check it out:

<div id="foo"></div>
<script>
  const div = document.querySelector('#foo');

  console.log(div.getAttribute('id')); // 'foo'
  console.log(div.id); // 'foo'

  div.id = 'bar';

  console.log(div.getAttribute('id')); // 'bar'
  console.log(div.id); // 'bar'
</script>

This seems to contradict the first example in the article, but the above works only because Element has an id getter and setter that “reflect” the id attribute.

When an attribute reflects a property, the property is the source of the data. Properties are updated when you set properties. When you read properties, properties are read.

For convenience, most specifications will create an equivalent property for each defined characteristic. Invalid in the example at the beginning of the article, because foo is not a specification-defined characteristic, so there is no foo attribute specified to reflect it.

Naming differences

This is relatively minor, but sometimes the name of a property is different from the name of the property it reflects.

In some cases, only attributes are case-sensitive:

  • On <img>, el.crossOrigin reflects the crossorigin attribute.
  • On all elements, el.ariaLabel reflects the aria-label attribute (aria reflector becomes cross-browser in late 2023. Until then, you can only use the attribute).

In some cases, names have to be changed due to old JavaScript reserved words:

  • On all elements, el.className reflects the class attribute.
  • On <label>, el.htmlFor reflects the for attribute.

Validation, type coercion and default values

Properties come with validation and default values, while attributes don’t:

const input = document.createElement('input');

console.log(input.getAttribute('type')); // null
console.log(input.type); // 'text'

input.type = 'number';

console.log(input.getAttribute('type')); // 'number'
console.log(input.type); // 'number'

input.type = 'foo';

console.log(input.getAttribute('type')); // 'foo'
console.log(input.type); // 'text'

In this case, validation is handled by the type getter. The setter allows the invalid value ‘foo’, but when the getter sees an invalid value or no value, it returns ‘text’.

Some properties perform type coercion:

<details open>…</details>
<script>
  const details = document.querySelector('details');

  console.log(details.getAttribute('open')); // ''
  console.log(details.open); // true

  details.open = false;

  console.log(details.getAttribute('open')); // null
  console.log(details.open); // false

  details.open = 'hello';

  console.log(details.getAttribute('open')); // ''
  console.log(details.open); // true
</script>

In this case, the open attribute is a boolean value that returns whether the feature exists. The setter also coerces the type – even if the setter gives ‘hello’ it will be converted to a boolean rather than going directly to the attribute.

Properties like img.height force property values ​​to be numbers. The setter converts the incoming value to a number and treats negative values ​​as 0.

value on input

value is an interesting attribute. There is a value attribute and a value attribute. However, the value attribute does not reflect the value attribute. In contrast, the defaultValue attribute reflects the value attribute.

In fact, the value attribute does not reflect any properties. This is not uncommon, there are a lot of properties like this (offsetWidth, parentNode, indeterminate, and many others on the checkbox for some reason).

Initially, the value attribute conforms to the defaultValue attribute. Then, once the value attribute is set via JavaScript or user interaction, it switches to the internal value. It’s like the implementation is roughly as follows:

class HTMLInputElement extends HTMLElement {
  get defaultValue() {
    return this.getAttribute('value') ?? '';
  }

  set defaultValue(newValue) {
    this.setAttribute('value', String(newValue));
  }

  #value = undefined;

  get value() {
    return this.#value ?? this.defaultValue;
  }

  set value(newValue) {
    this.#value = String(newValue);
  }

  // This happens when the associated form resets
  formResetCallback() {
    this.#value = undefined;
  }
}

so:

<input type="text" value="default" />
<script>
  const input = document.querySelector('input');

  console.log(input.getAttribute('value')); // 'default'
  console.log(input.value); // 'default'
  console.log(input.defaultValue); // 'default'

  input.defaultValue = 'new default';

  console.log(input.getAttribute('value')); // 'new default'
  console.log(input.value); // 'new default'
  console.log(input.defaultValue); // 'new default'

  // Here comes the mode switch:
  input.value = 'hello!';

  console.log(input.getAttribute('value')); // 'new default'
  console.log(input.value); // 'hello!'
  console.log(input.defaultValue); // 'new default'

  input.setAttribute('value', 'another new default');

  console.log(input.getAttribute('value')); // 'another new default'
  console.log(input.value); // 'hello!'
  console.log(input.defaultValue); // 'another new default'
</script>

This would make more sense if the value attribute was named defaultvalue. It’s too late now.

Properties applied to configuration

In my opinion, attributes should be used for configuration, while properties can contain state. I also believe that lightweight DOM trees should have a single owner.

In this sense, I think <input value> does a good job (except naming). The value attribute configures the default value, while the value attribute provides the current state.

It also makes sense that validation is applied when getting/setting properties, but not when getting/setting properties.

I say “in my opinion” because some recent HTML elements are different.

<details> and <dialog> elements indicate their open status through the open attribute, and browsers will automatically add/remove this attribute in response to user interaction.

I think this is a design error. It destroys the idea of ​​features being used for configuration, but more importantly, it means that the system responsible for maintaining the DOM (framework or native JS) needs to be prepared for changes to the DOM itself.

I think it should be:

<details defaultopen>…</details>

And has the details.open property to get/set the current state, and a CSS pseudo-class for positioning against that state.

Update: Simon Peters found some early design discussions on this issue .

I guess contenteditable also violates this contract, but…well…it’s an option that would cause a lot of damage.

How the framework handles differences

Back to the previous example:

<input className="…" type="…" aria-label="…" value="…" />

How does the framework handle this?

Preact and VueJS

In addition to a predefined set of properties they prefer, they will set the prop to a property if there is a propName in the element, and to a property otherwise. Basically, they prefer attributes over features. Their render-to-string method is just the opposite, and ignores content limited to properties.

setProperty in Preact .

shouldSetAsProp in VueJS .

React

React does exactly the opposite. will be set to attributes in addition to a predefined set of their preferred attributes. This makes their render-to-string methods logically similar.

This explains why custom elements don’t seem to work in React. Since they are custom, their properties are not in React’s “predefined list”, so they are set as attributes. Anything limited to attributes on a custom element will not work. This issue will be fixed in React 19 and for custom elements they will switch to Preact/VueJS models.

Interestingly, React popularly uses className instead of class in a place that looks like a feature. However, React sets the class attribute behind the scenes even if you use the property name instead of the attribute name .

setProp in React .

lit-html

Lit is handled slightly differently:

<input type="…" .value="…" />

It preserves the distinction between attributes and attributes, if you want to set an attribute instead of an attribute, you need to prefix the name with .

Expression documentation for Lit.

in conclusion

That’s basically everything I know about the difference between properties and characteristics. If there’s anything I missed, or you have any questions, let me know in the comments below!

Leave a Reply

Your email address will not be published. Required fields are marked *