Attributes and Observed — Complete Guide
In this tutorial, you will learn about Attributes and Observed. We cover key concepts, practical examples, and best practices to help you master this topic.
Custom element attributes are reflected as properties, with observedAttributes enabling reactive updates and attributeChangedCallback responding to changes.
What You'll Learn
- How to declare observed attributes
- How to reflect properties as attributes
- The difference between attributes and properties
- How to handle different attribute types (string, number, boolean, JSON)
- How to use getters and setters for reactivity
Why It Matters
Attributes are how HTML communicates configuration to components. Proper attribute handling makes your components feel native.
Real-World Use
- A slider reads min, max, value attributes and updates value as the user drags
- A tooltip watches the text attribute for content changes
- A chart reads data as a JSON attribute for chart configuration
flowchart LR A[HTML Attribute] --> B[observedAttributes] B --> C[attributeChangedCallback] C --> D[Component Updates] E[JS Property] --> F[Getter/Setter] F --> G[setAttribute] G --> C
Declaring Observed Attributes
The static observedAttributes getter tells the browser which attributes to watch.
class Tooltip extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
static get observedAttributes() {
return ['text', 'position', 'theme', 'delay'];
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue === newValue) return;
console.log('Attribute change:', name, '=', newValue);
switch (name) {
case 'text': this._updateText(newValue); break;
case 'position': this._updatePosition(newValue); break;
case 'theme': this._updateTheme(newValue); break;
case 'delay': this._updateDelay(parseInt(newValue) || 200); break;
}
}
connectedCallback() {
this.render();
}
render() {
const text = this.getAttribute('text') || 'Tooltip';
const pos = this.getAttribute('position') || 'top';
const theme = this.getAttribute('theme') || 'dark';
this.shadowRoot.innerHTML = '<slot></slot><div class="tooltip ' + pos + ' ' + theme + '">' + text + '</div>';
}
_updateText(text) {
const tip = this.shadowRoot.querySelector('.tooltip');
if (tip) tip.textContent = text;
}
_updatePosition(pos) {
const tip = this.shadowRoot.querySelector('.tooltip');
if (tip) tip.className = 'tooltip ' + pos + ' ' + (this.getAttribute('theme') || 'dark');
}
}
customElements.define('my-tooltip', Tooltip);
Expected output: Changing text, position, or theme attributes dynamically updates the tooltip.
Reflecting Properties to Attributes
For a native-feeling API, changes via JavaScript properties should reflect to attributes.
class ProgressRing extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
static get observedAttributes() {
return ['value', 'max', 'size', 'stroke-width', 'color'];
}
get value() {
return parseFloat(this.getAttribute('value')) || 0;
}
set value(val) {
this.setAttribute('value', val);
}
get max() {
return parseFloat(this.getAttribute('max')) || 100;
}
set max(val) {
this.setAttribute('max', val);
}
get percentage() {
return (this.value / this.max) * 100;
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue === newValue) return;
if (this.isConnected) this.render();
}
connectedCallback() {
this.render();
}
render() {
const value = this.value;
const max = this.max;
const size = parseInt(this.getAttribute('size')) || 120;
const sw = parseInt(this.getAttribute('stroke-width')) || 8;
const color = this.getAttribute('color') || '#3498db';
const pct = (value / max) * 100;
const r = (size - sw) / 2;
const circ = 2 * Math.PI * r;
const offset = circ - (pct / 100) * circ;
const c = size / 2;
this.shadowRoot.innerHTML = '<svg width="' + size + '" height="' + size + '" viewBox="0 0 ' + size + ' ' + size + '">'
+ '<circle cx="' + c + '" cy="' + c + '" r="' + r + '" fill="none" stroke="#e0e0e0" stroke-width="' + sw + '"/>'
+ '<circle cx="' + c + '" cy="' + c + '" r="' + r + '" fill="none" stroke="' + color + '" stroke-width="' + sw + '" stroke-dasharray="' + circ + '" stroke-dashoffset="' + offset + '" stroke-linecap="round" transform="rotate(-90 ' + c + ' ' + c + ')"/>'
+ '<text x="' + c + '" y="' + c + '" text-anchor="middle" dominant-baseline="central" font-size="' + (size * 0.25) + '" font-family="sans-serif" fill="#333">' + Math.round(pct) + '%</text>'
+ '</svg>';
}
}
customElements.define('progress-ring', ProgressRing);
Expected output: Both ring.value = 50 and ring.setAttribute('value', '75') work identically.
Handling Different Attribute Types
Attributes are always strings. Convert to appropriate types.
class ConfigPanel extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
static get observedAttributes() {
return ['data', 'enabled', 'count', 'options'];
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue === newValue) return;
switch (name) {
case 'data':
try { this._data = JSON.parse(newValue); } catch { this._data = {}; }
break;
case 'enabled':
this._enabled = newValue !== null;
break;
case 'count':
this._count = parseInt(newValue) || 0;
break;
case 'options':
this._options = newValue ? newValue.split(',').map(s => s.trim()) : [];
break;
}
console.log(name + ':', this['_' + name]);
}
connectedCallback() {
this.render();
}
render() {
this.shadowRoot.innerHTML = '<pre>' + JSON.stringify({
data: this._data, enabled: this._enabled, count: this._count, options: this._options
}, null, 2) + '</pre>';
}
}
customElements.define('config-panel', ConfigPanel);
Expected output: Each attribute type is parsed correctly: JSON, boolean, number, comma-separated list.
Common Mistakes
- Forgetting to return an array from observedAttributes
- Not checking oldValue === newValue before processing
- Using attributes for complex data without JSON Parsing
- Not converting attribute strings to proper types
- Reflecting every property to attribute unnecessarily
Practice Questions
- How do you declare observed attributes? static get observedAttributes() returning an array of attribute names.
- How do you reflect a property to an attribute? Create a setter that calls this.setAttribute().
- How do you read a boolean attribute? Check if getAttribute() returns non-null.
- Challenge: Create a slider component with min, max, step, and value attributes. Reflect the value property. Validate that value is within range.
FAQ
Mini Project
Build a configurable chart component that accepts data, type, colors, width, and height attributes. Reflect all as properties. Support data as a JSON attribute. Re-render when any attribute changes. Include validation for required attributes.
What's Next
Continue with Lesson 5: Custom Elements Built-In to learn about extending native HTML elements.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro