Reflecting Attributes
What is attribute reflection?
Section titled “What is attribute reflection?”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 getter / setter pair
Section titled “The getter / setter pair”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.
Declaring observed attributes
Section titled “Declaring observed attributes”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.
attributeChangedCallback
Section titled “attributeChangedCallback”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.
Full pattern
Section titled “Full pattern”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);Why reflection matters
Section titled “Why reflection matters”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"]orreflect-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.
Live demo
Section titled “Live demo”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.