Observed Attributes
Declaring watched attributes
Section titled “Declaring watched 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.
The attributeChangedCallback signature
Section titled “The attributeChangedCallback signature”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.
Full pattern
Section titled “Full pattern”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);Live demo
Section titled “Live demo”Try editing color="#4f46e5" to another CSS color value, or change the label text in the HTML panel — the badges re-render immediately.
Trade-offs
Section titled “Trade-offs”| Option | Benefit | Cost |
|---|---|---|
Use observedAttributes + attributeChangedCallback | Automatically reactive to attribute changes from outside (e.g. a framework or dev tools) — fits a declarative API | You 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 attribute | Gives you real typed control (not always a string) and avoids re-rendering on every attribute change | Not reactive to editing the attribute directly in HTML or dev tools — consumers have to know the element’s specific API |
Common mistakes
Section titled “Common mistakes”- Forgetting to declare static
observedAttributes— without this getter, or with it returning an empty array,attributeChangedCallbacknever fires no matter how many times the attribute changes, with no error to warn you, making it hard to debug. - Forgetting that
observedAttributesmust be static — it has to bestatic 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 === newValuebefore re-rendering — the browser can callattributeChangedCallbackeven when the value hasn’t actually changed (e.g. whileconnectedCallbacksyncs the initial value). Rendering without that check wastes CPU for no reason.
💡 Real-world examples
GitHub — the
<relative-time>element usesattributeChangedCallbackto re-render its text whenever thedatetimeattribute is updated from server-rendered HTML or external JavaScript. (github/relative-time-element)Shoelace / Web Awesome — this design-system component library uses
observedAttributesextensively so a component’s properties (e.g.size,variant,disabled) stay two-way synced with their HTML attributes automatically. (webawesome.com)