Skip to content

Observed Attributes

The browser only tracks attributes you explicitly opt in to. The static getter observedAttributes must return an array of attribute name strings:

static get observedAttributes() {
return ['color', 'label'];
}

Only attributes that appear in this array will ever trigger attributeChangedCallback. If the array is empty — or the getter is missing entirely — attribute changes are silently ignored.

When a watched attribute is added, changed, or removed, the browser calls:

attributeChangedCallback(name, oldValue, newValue) {
// name — the attribute name, e.g. 'color'
// oldValue — the previous value string, or null on first set
// newValue — the new value string, or null when removed
}

All three parameters are strings (or null). On the very first time an attribute is set, oldValue is null. When an attribute is removed with removeAttribute, newValue is null.

class AttrColor extends HTMLElement {
static get observedAttributes() {
return ['color', 'label'];
}
connectedCallback() {
this._render();
}
attributeChangedCallback(name, oldValue, newValue) {
this._render();
}
_render() {
var color = this.getAttribute('color') || '#6366f1';
var label = this.getAttribute('label') || '?';
this.innerHTML =
'<span style="' +
'display:inline-block;' +
'padding:.25rem .75rem;' +
'border-radius:9999px;' +
'background:' + color + ';' +
'color:#fff;' +
'font-weight:600;' +
'font-family:sans-serif;' +
'font-size:.875rem' +
'">' + label + '</span>';
}
}
customElements.define('attr-color', AttrColor);

Try editing color="#4f46e5" to another CSS color value, or change the label text in the HTML panel — the badges re-render immediately.

OptionBenefitCost
Use observedAttributes + attributeChangedCallbackAutomatically reactive to attribute changes from outside (e.g. a framework or dev tools) — fits a declarative APIYou have to parse the string value yourself every time, and risk re-rendering needlessly if you don’t check oldValue/newValue first
Have consumers call a method/setter instead of setting an attributeGives you real typed control (not always a string) and avoids re-rendering on every attribute changeNot reactive to editing the attribute directly in HTML or dev tools — consumers have to know the element’s specific API
  • Forgetting to declare static observedAttributes — without this getter, or with it returning an empty array, attributeChangedCallback never fires no matter how many times the attribute changes, with no error to warn you, making it hard to debug.
  • Forgetting that observedAttributes must be static — it has to be static get observedAttributes(), not an instance method. Put it in the wrong place and the getter is never called, behaving as if it wasn’t declared at all.
  • Not checking oldValue === newValue before re-rendering — the browser can call attributeChangedCallback even when the value hasn’t actually changed (e.g. while connectedCallback syncs the initial value). Rendering without that check wastes CPU for no reason.

💡 Real-world examples

GitHub — the <relative-time> element uses attributeChangedCallback to re-render its text whenever the datetime attribute is updated from server-rendered HTML or external JavaScript. (github/relative-time-element)

Shoelace / Web Awesome — this design-system component library uses observedAttributes extensively so a component’s properties (e.g. size, variant, disabled) stay two-way synced with their HTML attributes automatically. (webawesome.com)

What must static get observedAttributes() return?
What happens when you change an attribute that is NOT listed in observedAttributes?
What is the value of oldValue the very first time an attribute is set on an element?