attributeChangedCallback
This lesson explains attributeChangedCallback with clear examples, use cases, and best practices so you can build reusable Web Components confidently.
Web Components attributeChangedCallback Overview
attributeChangedCallback() is a Custom Elements lifecycle
method that runs whenever an observed attribute is added, changed, or
removed. It helps Web Components react to HTML attribute changes and keep
internal state, UI, and accessibility attributes synchronized.
This callback is commonly used for component variants, disabled states,
open or closed states, selected states, labels, sizes, themes, form
values, ARIA states, and property-to-attribute synchronization.
attributeChangedCallback List
- 1. Define observed attributes.
A component must declare which attributes it wants to observe using
static get observedAttributes().
class MyButton extends HTMLElement {
static get observedAttributes() {
return [
"variant",
"disabled"
];
}
}
- 2. Implement
attributeChangedCallback().
The callback receives the attribute name, old value, and new value.
attributeChangedCallback(
name,
oldValue,
newValue
) {
console.log(name);
console.log(oldValue);
console.log(newValue);
}
- 3. Ignore unchanged values.
Always return early when the old and new values are the same.
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue === newValue) {
return;
}
this.render();
}
- 4. React to added attributes.
When an attribute is added, oldValue is usually
null and newValue contains the new attribute
value.
const element =
document.querySelector("my-alert");
element.setAttribute(
"type",
"success"
);
- 5. React to removed attributes.
When an attribute is removed, newValue becomes
null.
attributeChangedCallback(name, oldValue, newValue) {
if (newValue === null) {
console.log(name + " was removed");
}
}
- 6. Handle boolean attributes.
Boolean attributes are true when present and false when absent.
get disabled() {
return this.hasAttribute("disabled");
}
set disabled(value) {
if (value) {
this.setAttribute("disabled", "");
} else {
this.removeAttribute("disabled");
}
}
- 7. Sync disabled state.
Use the callback to update internal buttons and accessibility state when
disabled changes.
updateDisabled() {
const button =
this.shadowRoot.querySelector("button");
const disabled =
this.hasAttribute("disabled");
button.disabled =
disabled;
this.setAttribute(
"aria-disabled",
String(disabled)
);
}
- 8. Handle string attributes.
String attributes such as label, variant, and
size can update text or styles.
updateLabel() {
const label =
this.getAttribute("label") || "Submit";
this.shadowRoot.querySelector("button")
.textContent = label;
}
- 9. Handle numeric attributes.
Convert attribute values to numbers before using them.
get count() {
return Number(
this.getAttribute("count") || 0
);
}
updateCount() {
this.shadowRoot.querySelector(".count")
.textContent = String(this.count);
}
- 10. Validate attribute values.
Only accept expected values to prevent broken states.
get variant() {
const allowed =
["primary", "secondary", "danger"];
const value =
this.getAttribute("variant");
return allowed.includes(value)
? value
: "primary";
}
- 11. Use switch statements for multiple attributes.
A switch statement keeps attribute handling organized.
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue === newValue) {
return;
}
switch (name) {
case "label":
this.updateLabel();
break;
case "disabled":
this.updateDisabled();
break;
case "variant":
this.updateVariant();
break;
}
}
- 12. Render safely when Shadow DOM is ready.
Attributes can change before the component is connected. Check that the
Shadow DOM exists before querying it.
updateLabel() {
if (!this.shadowRoot) {
return;
}
const button =
this.shadowRoot.querySelector("button");
if (button) {
button.textContent =
this.getAttribute("label") || "Submit";
}
}
- 13. Initial attributes trigger the callback.
If an observed attribute exists in HTML before upgrade,
attributeChangedCallback() can run when the element is
upgraded.
<my-alert type="success">
Saved successfully
</my-alert>
- 14. Use connectedCallback for initial render.
Use connectedCallback() to create DOM, then let
attributeChangedCallback() update state.
connectedCallback() {
if (!this.shadowRoot) {
this.attachShadow(
{ mode: "open" }
);
this.render();
}
this.updateLabel();
this.updateDisabled();
}
- 15. Reflect properties to attributes.
Property setters can reflect values to attributes so both JavaScript and
HTML usage stay consistent.
get label() {
return this.getAttribute("label") || "";
}
set label(value) {
this.setAttribute(
"label",
value
);
}
- 16. Avoid infinite loops.
Do not repeatedly set the same observed attribute inside
attributeChangedCallback() without guards.
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue === newValue) {
return;
}
this.updateFromAttribute(
name,
newValue
);
}
- 17. Normalize attribute values.
Convert raw attribute strings into normalized component state.
get size() {
const value =
this.getAttribute("size");
if (value === "small") {
return "small";
}
if (value === "large") {
return "large";
}
return "medium";
}
- 18. Update ARIA state from attributes.
Attribute changes should keep accessibility state in sync.
updateOpenState() {
const isOpen =
this.hasAttribute("open");
this.setAttribute(
"aria-expanded",
String(isOpen)
);
}
- 19. Update internal classes from attributes.
Attributes can drive visual variants without exposing internal class
names to users.
updateVariant() {
const panel =
this.shadowRoot.querySelector(".panel");
panel.className =
"panel panel-" + this.variant;
}
- 20. Use attributes for public HTML configuration.
Attributes are best for simple string, boolean, and numeric values that
users may set directly in HTML.
<my-button
variant="primary"
size="large"
label="Save">
</my-button>
- 21. Use properties for complex data.
Complex values such as objects, arrays, functions, and class instances
should usually be passed as properties, not attributes.
const table =
document.querySelector("my-table");
table.rows =
[
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" }
];
- 22. Convert JSON attributes carefully.
JSON in attributes can be fragile. Use try/catch and prefer properties
for large or complex data.
get config() {
try {
return JSON.parse(
this.getAttribute("config") || "{}"
);
} catch (error) {
return {};
}
}
- 23. Re-render only what changed.
For performance, update the specific DOM part affected by the changed
attribute instead of rebuilding the entire component every time.
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue === newValue) {
return;
}
if (name === "label") {
this.updateLabel();
}
}
- 24. Batch expensive updates when needed.
If several attributes may change together, batch rendering with a
scheduled update.
scheduleUpdate() {
if (this.updateQueued) {
return;
}
this.updateQueued =
true;
queueMicrotask(() => {
this.updateQueued =
false;
this.render();
});
}
- 25. Keep observed attributes lowercase.
HTML attribute names are case-insensitive in markup, so use lowercase or
kebab-case names.
static get observedAttributes() {
return [
"aria-label",
"data-state",
"max-count"
];
}
- 26. Map kebab-case attributes to camelCase properties.
Attributes often use kebab-case while JavaScript properties use
camelCase.
get maxCount() {
return Number(
this.getAttribute("max-count") || 0
);
}
set maxCount(value) {
this.setAttribute(
"max-count",
String(value)
);
}
- 27. Test attribute changes with JavaScript.
Components should update correctly when attributes change after initial
render.
const alert =
document.querySelector("my-alert");
alert.setAttribute(
"type",
"warning"
);
alert.removeAttribute("hidden");
- 28. Test attribute changes from HTML.
Components should initialize correctly when attributes are present in
markup.
<my-alert
type="success"
dismissible>
Saved successfully
</my-alert>
- 29. Document supported attributes.
Public Web Components should clearly document every supported attribute,
allowed values, default values, and related properties.
- 30. Use attribute changes for component API consistency.
Good attribute handling allows your Web Component to work from plain
HTML, JavaScript, frameworks, server-rendered markup, and design system
documentation examples.
Complete attributeChangedCallback Example
This example shows observed attributes, property reflection, boolean
attributes, validation, rendering, and accessibility synchronization.
class MyButton extends HTMLElement {
static get observedAttributes() {
return [
"label",
"variant",
"disabled"
];
}
constructor() {
super();
this.attachShadow(
{ mode: "open" }
);
this.shadowRoot.innerHTML =
`
<style>
button {
border: 1px solid currentColor;
border-radius: 0.5rem;
padding: 0.5rem 0.75rem;
}
button.primary {
font-weight: 700;
}
button.danger {
text-transform: uppercase;
}
</style>
<button type="button">
Submit
</button>
`;
}
connectedCallback() {
this.updateLabel();
this.updateVariant();
this.updateDisabled();
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue === newValue) {
return;
}
switch (name) {
case "label":
this.updateLabel();
break;
case "variant":
this.updateVariant();
break;
case "disabled":
this.updateDisabled();
break;
}
}
get label() {
return this.getAttribute("label") || "Submit";
}
set label(value) {
this.setAttribute("label", value);
}
get variant() {
const allowed =
["primary", "secondary", "danger"];
const value =
this.getAttribute("variant");
return allowed.includes(value)
? value
: "primary";
}
set variant(value) {
this.setAttribute("variant", value);
}
get disabled() {
return this.hasAttribute("disabled");
}
set disabled(value) {
if (value) {
this.setAttribute("disabled", "");
} else {
this.removeAttribute("disabled");
}
}
updateLabel() {
const button =
this.shadowRoot.querySelector("button");
button.textContent =
this.label;
}
updateVariant() {
const button =
this.shadowRoot.querySelector("button");
button.className =
this.variant;
}
updateDisabled() {
const button =
this.shadowRoot.querySelector("button");
button.disabled =
this.disabled;
this.setAttribute(
"aria-disabled",
String(this.disabled)
);
}
}
customElements.define(
"my-button",
MyButton
);
<my-button
label="Delete"
variant="danger">
</my-button>
const button =
document.querySelector("my-button");
button.label =
"Save";
button.disabled =
true;
Lifecycle Callback Comparison
| Callback | When It Runs | Common Use |
constructor() | When the element instance is created. | Initialize state and attach Shadow DOM. |
connectedCallback() | When the element is added to the DOM. | Render DOM, add listeners, start observers. |
attributeChangedCallback() | When observed attributes are added, changed, or removed. | Sync attributes with UI and state. |
disconnectedCallback() | When the element is removed from the DOM. | Clean up timers, listeners, and observers. |
Common attributeChangedCallback Mistakes
-
Forgetting to define
observedAttributes.
-
Expecting the callback to run for attributes that are not observed.
-
Not checking whether
oldValue and
newValue are the same.
-
Setting observed attributes repeatedly and causing loops.
-
Querying Shadow DOM before it exists.
-
Treating boolean attributes like normal string values.
-
Using attributes for complex objects instead of properties.
-
Not validating allowed attribute values.
-
Re-rendering the full component for every small attribute change.
-
Forgetting to update ARIA states and internal controls.
attributeChangedCallback Best Practices
-
Observe only public attributes that should trigger updates.
-
Return early when values are unchanged.
-
Use property getters and setters to reflect simple values.
-
Use boolean attributes correctly with
hasAttribute().
-
Normalize and validate attribute values.
-
Update only the affected DOM parts when possible.
-
Keep accessibility states synchronized.
-
Use properties instead of attributes for complex data.
-
Document attributes, defaults, and allowed values.
-
Test initial attributes, runtime changes, removals, and property
updates.
Pro Tip
Use attributeChangedCallback() for simple public HTML
configuration such as variant, size,
disabled, open, and label. Use
JavaScript properties for complex data like arrays, objects, callbacks,
and service instances.