Skip to content

Attributes and Observed — Complete Guide

DodaTech Updated 2026-06-28 5 min read

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

  1. Forgetting to return an array from observedAttributes
  2. Not checking oldValue === newValue before processing
  3. Using attributes for complex data without JSON Parsing
  4. Not converting attribute strings to proper types
  5. Reflecting every property to attribute unnecessarily

Practice Questions

  1. How do you declare observed attributes? static get observedAttributes() returning an array of attribute names.
  2. How do you reflect a property to an attribute? Create a setter that calls this.setAttribute().
  3. How do you read a boolean attribute? Check if getAttribute() returns non-null.
  4. Challenge: Create a slider component with min, max, step, and value attributes. Reflect the value property. Validate that value is within range.

FAQ

What happens if observedAttributes returns an empty array?

No attributes are monitored. attributeChangedCallback never fires.

Can I observe attributes that are not in observedAttributes?

No. Only attributes listed in observedAttributes trigger attributeChangedCallback.

Are attribute values always strings?

Yes. HTML attributes are always strings. Numbers, booleans, and objects must be parsed.

Does removing an attribute trigger attributeChangedCallback?

Yes. The newValue is null when an attribute is removed.

Can I set attribute values from inside the component?

Yes. Calling setAttribute inside the component triggers attributeChangedCallback again. Guard against infinite loops.

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