Attributes & Properties
Every HTML element carries information in two parallel worlds: the attributes you write in the markup (id, href, value, disabled) and the properties exposed on the live DOM object in JavaScript. They usually look identical, but they are not the same thing — and confusing them is the source of countless “why won’t my form update?” bugs. This page covers the attribute methods (getAttribute, setAttribute, hasAttribute, removeAttribute), the crucial attribute-vs-property distinction, and the dataset API for data-* custom attributes.
Attributes vs. properties
When the browser parses HTML, it reads the attributes in your markup and uses them to build a DOM element object. Many attributes get reflected onto the object as properties — but the relationship is a one-time initialization for some of them, not a permanent two-way binding.
The classic example is a form input. The value attribute records the element’s initial (default) value as written in the HTML. The value property holds the element’s current value, which changes as the user types. They diverge the moment someone edits the field.
<input id="name" value="Ada" />
const input = document.querySelector("#name");
// user types "Grace" into the field…
input.getAttribute("value"); // "Ada" ← still the original markup
input.value; // "Grace" ← the live, current value
Output:
"Ada"
"Grace"
The same gotcha applies to checked on checkboxes: the checked attribute is the default state, while the .checked property is the live state.
Gotcha: To read what the user actually sees or typed, use the property (
el.value,el.checked). UsegetAttributeonly when you specifically want the original markup value or a non-reflected custom attribute.
| You want… | Use |
|---|---|
| The current value the user sees | el.value (property) |
| Whether a box is checked right now | el.checked (property) |
| The original markup value | el.getAttribute("value") |
| A custom/non-standard attribute | getAttribute / dataset |
The attribute methods
For attributes there are four core methods, all called on an element. They always work with strings, since attribute values are textual in the markup.
const link = document.querySelector("a");
link.getAttribute("href"); // read → "/about" (or null if absent)
link.setAttribute("href", "/home"); // write/overwrite
link.hasAttribute("target"); // boolean test
link.removeAttribute("target"); // delete the attribute
A few precise behaviors worth knowing:
getAttributereturnsnullwhen the attribute is missing (notundefined, not"").setAttributealways coerces the value to a string:setAttribute("data-n", 5)stores"5".- For boolean attributes like
disabled, presence alone means true — the value is irrelevant.setAttribute("disabled", "false")still disables the element, because the attribute exists. Toggle these via the property instead:button.disabled = false.
const btn = document.querySelector("button");
btn.disabled = true; // ✅ adds the attribute, disables it
btn.disabled = false; // ✅ removes it
btn.setAttribute("disabled", "false"); // ❌ STILL disabled — presence wins
Custom data with dataset
Standard attributes have dedicated meanings, so you shouldn’t invent your own. Instead, HTML reserves the data-* namespace for arbitrary custom data, and the DOM exposes all of them through the convenient element.dataset object.
Names map between the two worlds via camelCase: data-user-id in HTML becomes dataset.userId in JavaScript. Reading and writing dataset properties transparently updates the underlying data-* attribute.
<article id="post" data-post-id="42" data-author-name="Linus"></article>
const post = document.querySelector("#post");
post.dataset.postId; // "42" (note: always a string)
post.dataset.authorName; // "Linus"
post.dataset.postId = 99; // sets data-post-id="99"
post.dataset.published = true; // adds data-published="true"
delete post.dataset.authorName; // removes the attribute
Output:
"42"
"Linus"
Because every value is a string, parse when you need a real type: Number(el.dataset.postId) or el.dataset.active === "true".
An interactive demo
The pen below stores configuration on the markup with data-* attributes and reads it back at click time — a common pattern for event delegation, where one listener handles many elements and pulls per-element data from dataset.
<button data-color="tomato" data-label="Warm">A</button>
<button data-color="cornflowerblue" data-label="Cool">B</button>
<p id="out">Click a button…</p>
<script>
document.body.addEventListener("click", (e) => {
const btn = e.target.closest("button");
if (!btn) return;
document.body.style.background = btn.dataset.color;
document.querySelector("#out").textContent =
`${btn.dataset.label} (${btn.dataset.color})`;
});
</script>
This pen demonstrates the attribute-vs-property split live: type into the field and watch the property change while the attribute stays put.
<input id="field" value="default" />
<button id="show">Compare</button>
<button id="reset">Reset to attribute</button>
<pre id="log"></pre>
<script>
const field = document.querySelector("#field");
const log = document.querySelector("#log");
document.querySelector("#show").addEventListener("click", () => {
log.textContent =
`property .value: ${field.value}\n` +
`getAttribute("value"): ${field.getAttribute("value")}`;
});
document.querySelector("#reset").addEventListener("click", () => {
field.value = field.getAttribute("value"); // restore the markup default
});
</script>
Best Practices
- Reach for the property (
el.value,el.checked,el.disabled) for live state; usegetAttributeonly for original markup or non-reflected attributes. - Toggle boolean attributes with the property, never
setAttribute("disabled", "false")— presence is what counts. - Store custom data in
data-*attributes viadataset, never in invented non-standard attributes. - Remember every attribute and
datasetvalue is a string; convert withNumber()or compare against"true"explicitly. - Use
removeAttribute(ordelete el.dataset.x) to delete an attribute; setting it to""leaves it present and truthy for booleans. - Check existence with
hasAttributerather than relying ongetAttribute(...) !== nullwhen intent matters.