HTML attributes vs DOM properties is a key distinction in JavaScript: attributes belong to an element’s markup, while properties are values exposed by its DOM object. Some properties reflect attributes, so reading or changing one can affect the other; many do not. Knowing which layer you are working with helps prevent subtle bugs.
HTML Attributes and DOM Properties: The Core Difference
Consider this markup:
<input id="email" type="email" value="start@example.com">
The text id, type, and value in the start tag are HTML attributes. The browser parses the markup and creates an HTMLInputElement object. JavaScript can then read properties such as input.id, input.type, and input.value.
Use attributes when you need to inspect or change the element’s markup-level data. Use properties when you need the DOM object’s current, typed state. The relationship depends on the specific property: id reflects the id attribute, while an input’s live value can change independently after the user types.
Reading and Changing Attributes
The Element methods provide explicit access to attributes. Attribute names are case-insensitive in HTML documents and are normally written in lowercase.
<button id="save" class="action" data-mode="compact">Save</button>
<script>
const button = document.querySelector("#save");
console.log(button.getAttribute("class")); // "action"
console.log(button.hasAttribute("disabled")); // false
button.setAttribute("title", "Save your changes");
button.setAttribute("aria-pressed", "true");
button.removeAttribute("data-mode");
console.log(button.hasAttribute("data-mode")); // false
</script>
getAttribute(name) returns the attribute’s string value, or null if it is absent. setAttribute(name, value) creates or replaces it; the value is converted to a string. hasAttribute(name) checks presence, which matters for boolean attributes. removeAttribute(name) removes it.
These methods work with custom attributes and attributes that do not have a convenient DOM property. For standard HTML attributes, property access may be shorter, but the two interfaces are not interchangeable in every case.
Reflected Properties
A property is called reflected when the platform defines it to correspond to an HTML attribute. For example, an element’s id property reflects its id attribute:
<div id="panel"></div>
<script>
const panel = document.querySelector("#panel");
console.log(panel.id); // "panel"
console.log(panel.getAttribute("id")); // "panel"
panel.id = "settings";
console.log(panel.getAttribute("id")); // "settings"
panel.setAttribute("id", "profile");
console.log(panel.id); // "profile"
</script>
Reflection is defined separately for each attribute and property. A reflected property may normalize or convert the value. For example, input.type is a string property with browser-defined behavior, and URL-related properties may expose resolved URLs even when the attribute contains a relative path.
Boolean Attributes Are Based on Presence
HTML boolean attributes represent true when the attribute is present and false when it is absent. The written value does not determine truth:
<button disabled>Unavailable</button>
<button disabled="">Also unavailable</button>
<button disabled="false">Still unavailable</button>
All three buttons are disabled because disabled is present. To enable a button, remove the attribute or set the reflected boolean property to false:
const button = document.querySelector("button");
button.disabled = false; // removes the reflected disabled state
// Equivalent intent:
button.removeAttribute("disabled");
button.disabled = true; // sets disabled
// Presence also makes it true:
button.setAttribute("disabled", "");
Boolean properties return JavaScript booleans, while getAttribute() returns a string or null. Prefer element.disabled to inspect the current disabled state. Avoid setAttribute("disabled", false): it writes the string "false", which still means the boolean attribute is present.
Default Values and Current Form State
Form controls show why attributes and properties can diverge. An input’s value attribute supplies its default value. The value property represents its current value, which may change as a person types.
<input id="name" value="Ari">
<script>
const nameInput = document.querySelector("#name");
console.log(nameInput.getAttribute("value")); // "Ari"
console.log(nameInput.defaultValue); // "Ari"
console.log(nameInput.value); // "Ari"
nameInput.value = "Sam"; // changes the current form value
console.log(nameInput.getAttribute("value")); // "Ari"
console.log(nameInput.value); // "Sam"
</script>
Checkboxes have a similar distinction: checked in markup establishes the default, represented by defaultChecked; the live checked property reflects whether the box is currently selected. After a user toggles it, the live property can differ from the original attribute. Use the live property when handling form interaction.
Working with data-* Attributes and dataset
Custom data-* attributes store application-specific strings in HTML. JavaScript exposes them through the element’s dataset object, a DOMStringMap:
<article id="card" data-user-id="42" data-theme-name="dark">
Profile
</article>
<script>
const card = document.querySelector("#card");
console.log(card.dataset.userId); // "42"
console.log(card.dataset.themeName); // "dark"
card.dataset.state = "open"; // creates data-state="open"
delete card.dataset.themeName; // removes data-theme-name
console.log(card.getAttribute("data-user-id")); // "42"
</script>
Hyphenated names become camelCase in dataset: data-user-id maps to dataset.userId. Values remain strings, so convert them when you need another type:
const userId = Number(card.dataset.userId);
const isReady = card.dataset.ready === "true";
Use data attributes for small bits of non-sensitive data associated with an element. They are visible in the DOM and are not a secure storage mechanism.
Practical Example: Attribute Defaults and Live State
This runnable example updates a message when a checkbox changes. The data-message attribute supplies custom text, while the checkbox’s checked property tells the script its current state.
<label>
<input id="updates" type="checkbox" data-message="Email updates enabled">
Receive email updates
</label>
<p id="status" aria-live="polite"></p>
<script>
const updates = document.querySelector("#updates");
const status = document.querySelector("#status");
function renderStatus() {
status.textContent = updates.checked
? updates.dataset.message
: "Email updates disabled";
}
updates.addEventListener("change", renderStatus);
renderStatus();
</script>
The script reads current interaction state from checked rather than asking whether the original checked attribute exists. It reads the custom string through dataset and updates visible text safely with textContent.
