Skip to content

Properties vs Attributes

Attributes live in HTML and are always strings

Section titled “Attributes live in HTML and are always strings”

When you write <my-card count="5" label="hello"> in HTML, the browser stores those as attribute nodes — and every attribute value is a string, no matter what you type. Calling element.getAttribute('count') returns the string "5", not the number 5.

const el = document.querySelector('my-card');
console.log(el.getAttribute('count')); // "5" (string)
console.log(el.getAttribute('label')); // "hello" (string)
console.log(el.getAttribute('missing')); // null (not present)

This is important: getAttribute returns either a string or null — never a boolean, number, array, or object.

Properties are regular JavaScript object properties on the element instance. They can be any JS type: booleans, numbers, arrays, objects, or functions. You access them with dot notation, not getAttribute.

el.count = 5; // number
el.rows = [{ id: 1 }]; // array
el.config = { theme: 'dark', size: 'lg' }; // object
el.disabled = true; // boolean

These property values are not serialised back into the DOM unless your component explicitly does so. A property set on an element instance lives only in JavaScript memory.

Boolean attributes: check presence, not value

Section titled “Boolean attributes: check presence, not value”

HTML boolean attributes work by presence or absence — if the attribute exists on the element, it is true; if it is absent, it is false. The value of the attribute string does not matter.

That means <my-btn disabled=""> and <my-btn disabled="false"> are both disabled because the attribute is present. Only <my-btn> (no attribute at all) means not disabled.

Never compare getAttribute('disabled') === 'true' or === true. Always use hasAttribute or check the return value against null.

// Boolean attribute — check presence, not value
class MyButton extends HTMLElement {
get disabled() { return this.hasAttribute('disabled'); }
set disabled(v) { v ? this.setAttribute('disabled', '') : this.removeAttribute('disabled'); }
}

Notice the setter sets the attribute to the empty string '' when truthy — that is the idiomatic HTML pattern. The getter ignores whatever string is stored; it just asks: is the attribute there at all?

Rich data: use a property, not an attribute

Section titled “Rich data: use a property, not an attribute”

When you need to pass an array, an object, or any non-string value into a component, a JS property setter is the right tool. Trying to serialize objects into attribute strings (e.g. data="[object Object]") is fragile and lossy.

// Rich data — use a property, not an attribute
class DataTable extends HTMLElement {
set rows(data) { // data is an array of objects
this._rows = data;
this._render();
}
get rows() { return this._rows || []; }
_render() { /* ... */ }
}

Then from JavaScript:

const table = document.querySelector('data-table');
table.rows = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
];

The setter receives the actual array — no parsing required. When the data changes, just assign the property again and _render runs.

The <user-card> element below combines both techniques:

  • A boolean attribute premium — when present it renders a crown
  • A property setter profile — accepts an object { name, role } and re-renders

The HTML sets the premium attribute directly in markup. The JavaScript then assigns el.profile = { name: 'Alice', role: 'Admin' } — a plain object — triggering the property setter and re-rendering the card.

ScenarioUse
Simple string config (label, type, variant)Attribute
Boolean flag (disabled, hidden, selected)Boolean attribute + hasAttribute
Number, object, array, or functionProperty setter
Value set from HTML markup onlyAttribute
Value set from JavaScriptEither — prefer property for non-strings
What type does `getAttribute` always return?
How should you check if a boolean attribute like `disabled` is present?
When is using a JS property better than an HTML attribute?
Setting `<my-el count="5">` — what is the type of `this.getAttribute('count')`?