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 JavaScript values
Section titled “Properties are JavaScript values”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; // numberel.rows = [{ id: 1 }]; // arrayel.config = { theme: 'dark', size: 'lg' }; // objectel.disabled = true; // booleanThese 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 valueclass 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 attributeclass 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.
Live demo
Section titled “Live demo”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.
Summary: which to use?
Section titled “Summary: which to use?”| Scenario | Use |
|---|---|
| Simple string config (label, type, variant) | Attribute |
| Boolean flag (disabled, hidden, selected) | Boolean attribute + hasAttribute |
| Number, object, array, or function | Property setter |
| Value set from HTML markup only | Attribute |
| Value set from JavaScript | Either — prefer property for non-strings |
Trade-offs
Section titled “Trade-offs”| Option | Benefit | Cost |
|---|---|---|
| Attribute | Visible directly in HTML markup and DevTools, usable as a CSS attribute selector, works with server-rendered HTML with no JavaScript needed | Always a string — you can’t pass rich data like an array or object directly |
| Property | Accepts any JS type — object, array, or function — and is faster to access via dot notation | Doesn’t show up in HTML markup or the DevTools Elements panel, making it harder to debug from markup alone |
Common mistakes
Section titled “Common mistakes”- Comparing
getAttribute('disabled') === 'true'— a boolean attribute works by presence, not by its string value. Always usehasAttributeinstead. - Using
JSON.stringify()to cram an object into an attribute string, e.g.data='{"a":1}'— an attribute can only hold a string, which is fragile and easy to parse incorrectly. Pass it through a property setter directly instead. - Expecting the attribute to update automatically after setting a property — without writing your own reflection pattern (a getter/setter that calls
setAttribute), setting a bare property never syncs back to the attribute, leaving the markup and the JS state out of sync.
💡 Real-world examples
The browser’s native
<input>element is the clearest example of this distinction — thevalueattribute is the default value set from HTML, while the.valueproperty is the current value the user is typing right now. The two diverge the moment the user starts typing.GitHub’s Catalyst framework recommends custom elements always expose a property for rich data, and reserve attributes only for simple string config values. (github/catalyst)