Skip to content

Reflecting Attributes

A custom element exposes its state in two ways at the same time: as a JavaScript property (e.g. element.label) and as an HTML attribute (e.g. label="Hello"). Reflection means keeping both in sync — writing to one automatically updates the other.

The native <input> element does this for value, disabled, checked, and others. You can give your own elements the same ergonomics.

The reflection pattern is a JavaScript getter and setter pair that delegates to getAttribute and setAttribute:

get label() {
return this.getAttribute('label') || '';
}
set label(v) {
this.setAttribute('label', v);
}

When external code reads element.label, the getter returns the current attribute value. When code assigns element.label = 'X', the setter calls setAttribute, so the HTML attribute is updated immediately. getAttribute('label') and element.label will always agree.

For attributeChangedCallback to fire — whether the attribute was changed from HTML, DevTools, or JavaScript — the attribute name must be listed in the static observedAttributes getter:

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

The browser reads this list once when the class is registered. Attributes not in the list are silently ignored.

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

attributeChangedCallback(name, oldValue, newValue) {
// name — which attribute changed, e.g. 'label'
// oldValue — previous value string, or null on first set
// newValue — new value string, or null when removed
this._render();
}

Because the setter calls setAttribute, setting element.label = 'X' triggers attributeChangedCallback just as if you had written element.setAttribute('label', 'X') directly. The callback is your single source of truth for re-rendering.

class MyBadge extends HTMLElement {
static get observedAttributes() { return ['label', 'color']; }
get label() { return this.getAttribute('label') || ''; }
set label(v) { this.setAttribute('label', v); }
get color() { return this.getAttribute('color') || 'blue'; }
set color(v) { this.setAttribute('color', v); }
attributeChangedCallback(name, oldValue, newValue) {
this._render();
}
connectedCallback() { this._render(); }
_render() {
this.textContent = this.label;
this.style.color = this.color;
}
}
customElements.define('my-badge', MyBadge);

Reflecting attributes gives your element several practical advantages:

  • DevTools visibility — attribute values appear directly in the Elements panel without expanding any shadow DOM.
  • CSS attribute selectors — you can style reflect-badge[color="#7c3aed"] or reflect-badge[label] without any extra classes.
  • Server-side HTML — the initial state can be baked into the markup; no JavaScript is needed to set the first value.
  • Framework interoperability — frameworks like Vue, Svelte, and Angular bind to attributes by default. Reflected properties mean <my-badge label={x}> just works.

After 1.2 seconds the element’s label property is set from JavaScript. The #log line confirms that element.label and getAttribute('label') return the same string — proof that the setter and the attribute are perfectly in sync.

What does the `set label(v)` setter do in the reflection pattern?
What is required for `attributeChangedCallback` to fire?
Which CSS selector can you use when an attribute is reflected?