Skip to content

Observed Attributes

This lesson explains observed attributes in Web Components, how to watch attribute changes with attributeChangedCallback, and how to keep component UI in sync with declarative HTML.

What Are Observed Attributes?

Observed attributes are HTML attributes that a custom element chooses to watch. When one of those attributes changes, the browser calls attributeChangedCallback() so the component can update its UI or internal state.

You declare them with the static observedAttributes getter. This is a core part of building reactive Web Components that respond to declarative HTML.

Concept Description
observedAttributes Static list of attributes to watch
attributeChangedCallback() Runs when an observed attribute changes
Trigger setAttribute(), removeAttribute(), HTML updates
Arguments name, oldValue, newValue
Best For Declarative config from HTML and SSR
Works With Properties, rendering, and accessibility state

Basic Observed Attributes Example

class StatusBadge extends HTMLElement {
  static get observedAttributes() {
    return ["variant"];
  }

  connectedCallback() {
    this.render();
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (oldValue === newValue) return;
    this.render();
  }

  render() {
    const variant =
      this.getAttribute("variant") || "info";
    this.textContent = `Status: ${variant}`;
  }
}

customElements.define("status-badge", StatusBadge);
<status-badge variant="success"></status-badge>

<script>
  const badge = document.querySelector("status-badge");
  badge.setAttribute("variant", "danger");
</script>

When variant changes, the component re-renders and reflects the new attribute value.

How Observed Attributes Work

Step What Happens
1. Declare watched names Return attribute names from observedAttributes
2. Attribute changes Browser detects update on the host element
3. Callback fires attributeChangedCallback() is invoked
4. Component reacts Update UI, classes, ARIA, or internal state
5. Render stays in sync HTML and component output match

Watching Multiple Attributes

class AppButton extends HTMLElement {
  static get observedAttributes() {
    return ["variant", "size", "disabled"];
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (oldValue === newValue) return;

    if (name === "variant") this.updateVariant(newValue);
    if (name === "size") this.updateSize(newValue);
    if (name === "disabled") this.updateDisabled(newValue);
  }
}

List every attribute the component should react to. Handle each change in one callback or delegate to focused update methods.

Boolean Observed Attributes

class TogglePanel extends HTMLElement {
  static get observedAttributes() {
    return ["open"];
  }

  attributeChangedCallback(name) {
    if (name === "open") {
      const isOpen = this.hasAttribute("open");
      this.shadowRoot.querySelector(".panel").hidden = !isOpen;
      this.setAttribute("aria-expanded", String(isOpen));
    }
  }
}
<toggle-panel open>
  Panel content
</toggle-panel>

For boolean attributes, use hasAttribute() instead of relying only on the attribute value string.

Initial Attributes and First Render

class AlertBox extends HTMLElement {
  static get observedAttributes() {
    return ["type", "message"];
  }

  connectedCallback() {
    this.render();
  }

  attributeChangedCallback() {
    if (!this.isConnected) return;
    this.render();
  }

  render() {
    const type = this.getAttribute("type") || "info";
    const message = this.getAttribute("message") || "";
    this.innerHTML = `
      <div class="alert alert-${type}" role="alert">
        ${message}
      </div>
    `;
  }
}

attributeChangedCallback() may run before the element is connected to the DOM. Many components render once in connectedCallback() and ignore disconnected updates.

attributeChangedCallback Arguments

attributeChangedCallback(name, oldValue, newValue) {
  console.log(name);      // "variant"
  console.log(oldValue);  // "info"
  console.log(newValue);  // "danger"
}
Argument Description
name Name of the changed attribute
oldValue Previous attribute value
newValue New attribute value

Performance Tips

attributeChangedCallback(name, oldValue, newValue) {
  if (oldValue === newValue) return;

  this._pendingRender = true;
  queueMicrotask(() => {
    if (!this._pendingRender) return;
    this._pendingRender = false;
    this.render();
  });
}
  • Skip work when oldValue === newValue.
  • Avoid rendering on every attribute change if several update at once.
  • Batch DOM writes when multiple attributes change together.
  • Only observe attributes the component truly needs.

Observed Attributes and Properties

class UserChip extends HTMLElement {
  static get observedAttributes() {
    return ["name", "role"];
  }

  get name() {
    return this.getAttribute("name") || "";
  }

  set name(value) {
    this.setAttribute("name", value);
  }

  attributeChangedCallback() {
    this.render();
  }
}

Observed attributes work best when properties and attributes stay synchronized. Changing a property can update an attribute, which then triggers the callback.

Attributes That Are Not Observed

// "hidden-ui" is NOT in observedAttributes
badge.setAttribute("hidden-ui", "true");
// attributeChangedCallback will NOT run

Only attributes listed in observedAttributes trigger the callback. If you need to react to other attributes, add them to the observed list.

When to Use Observed Attributes

  • The component is configured through HTML attributes.
  • You need SSR-friendly declarative APIs.
  • Attribute changes should update UI or accessibility state.
  • You reflect properties back to attributes.
  • Frameworks or pages set config with setAttribute().
  • Variant, size, open, and disabled states come from markup.

Common Observed Attribute Use Cases

  • variant and size on buttons and badges.
  • open on dialogs, drawers, and accordions.
  • disabled on form controls.
  • value and placeholder on inputs.
  • type on alerts and status components.
  • selected and active on tabs and menus.

Observed Attributes Best Practices

  • Keep observedAttributes small and intentional.
  • Compare oldValue and newValue before re-rendering.
  • Render once in connectedCallback() for initial state.
  • Use hasAttribute() for boolean attributes.
  • Sync important properties and attributes consistently.
  • Document which attributes are supported and what they do.
  • Update ARIA and visual state from attribute changes.

Common Observed Attribute Mistakes

  • Forgetting to list an attribute in observedAttributes.
  • Assuming all attribute changes trigger the callback automatically.
  • Re-rendering on every callback without checking value changes.
  • Not handling initial attributes before the element connects.
  • Treating boolean attributes like normal string values only.
  • Observing too many attributes and causing unnecessary renders.
  • Updating attributes inside the callback without guards, causing loops.

Key Takeaways

  • observedAttributes defines which attributes a component watches.
  • attributeChangedCallback() reacts to those changes.
  • Observed attributes power declarative, HTML-driven component APIs.
  • They work best with rendering, boolean state, and property sync.
  • Careful observation keeps Web Components reactive and efficient.

Pro Tip

Treat observedAttributes as your component's declarative config list. If an attribute is part of the public API and should change component behavior, observe it and document it.