DOM Attributes
HTML attributes look simple — href="/about", checked, data-id="42". The DOM exposes two ways to read and write them, and they don't always agree. This is the famous attribute vs property distinction, and it trips up everyone at least once. Once you understand the model, the small set of attribute methods becomes intuitive.
The four core methods
const a = document.querySelector("a");
a.getAttribute("href"); // "/about"
a.setAttribute("href", "/contact");
a.hasAttribute("download"); // false
a.removeAttribute("title");These four methods work on every element. They read and write the attributes — the strings exactly as they appear in HTML (or as you set them). They are case-insensitive (HREF and href are the same).
Attributes vs properties — the key insight
Every standard HTML attribute is mirrored as a JavaScript property on the DOM element. The attribute lives in the HTML; the property lives on the JavaScript object. For most attributes they stay in sync — but a few intentionally diverge.
Attribute — the original string from the markup. Read/written with
getAttribute/setAttribute.Property — the live value on the JavaScript object. Read/written with
el.href,el.checked, etc.For simple cases (
id,className, mostdata-*) they mirror each other.For some inputs the property tracks the current state while the attribute keeps the initial state. That is where bugs come from.
checked, value, and href: the gotchas
form.html
<input id="agree" type="checkbox" checked> <input id="name" type="text" value="Ada"> <a id="link" href="/about">About</a>
checkbox: property is live, attribute is initial
const cb = document.getElementById("agree");
cb.checked; // true — current state
cb.getAttribute("checked"); // "" — string presence in HTML
cb.checked = false; // user-visible: now unchecked
cb.getAttribute("checked"); // "" — attribute unchanged!
cb.hasAttribute("checked"); // true — still theretext input: same idea with value
const name = document.getElementById("name");
name.value; // "Ada"
name.getAttribute("value"); // "Ada"
name.value = "Lin"; // user types — current value
name.value; // "Lin"
name.getAttribute("value"); // "Ada" — attribute still holds the default
name.defaultValue; // "Ada" — same as the attributehref: attribute is raw, property is resolved
const a = document.getElementById("link");
a.getAttribute("href"); // "/about"
a.href; // "https://example.com/about" — fully resolved URLBoolean attributes
Attributes like checked, disabled, readonly, required, autoplay are boolean attributes — their presence means true, their absence means false. The value of the attribute is ignored.
<input disabled> <input disabled="false"> <!-- still disabled! --> <input disabled="disabled"> <!-- old-school but valid -->
input.disabled = true; // sets it
input.disabled = false; // unsets it
input.removeAttribute("disabled"); // also unsets it
input.setAttribute("disabled", "false"); // STILL disabled (presence wins)data-* attributes and the dataset object
HTML lets you store arbitrary metadata on elements with attributes whose names start with data-. The DOM mirrors those onto a single dataset object with camelCase keys.
<li class="task" data-id="42" data-user-name="ada">Buy milk</li>
const li = document.querySelector(".task");
li.dataset.id; // "42"
li.dataset.userName; // "ada" — note: data-user-name → userName
li.dataset.priority = "high";
// HTML is now: <li ... data-priority="high">
delete li.dataset.id;
// removes the data-id attributeValues are always strings. Convert with
Number()/JSON.parse()if needed.Hyphens in the attribute become camelCase on the dataset.
data-*is the standard escape hatch for storing IDs, types, flags or small bits of JSON on an element — perfect for event delegation.
why this is so handy
document.querySelector(".list").addEventListener("click", (e) => {
const li = e.target.closest(".task");
if (!li) return;
const id = Number(li.dataset.id);
console.log("task id =", id);
});task id = 42
Reading non-standard attributes safely
If you set a custom attribute like foo="bar" on an element, only the attribute API can see it — there is no el.foo property created.
const el = document.createElement("div");
el.setAttribute("foo", "bar");
el.foo; // undefined
el.getAttribute("foo"); // "bar"This is why frameworks lean on data-* — those are standard attributes with a well-defined mirror, and they pass HTML validators.
Quick reference
Standard attribute with a property (
href,id,value,checked,className) → use the property.Boolean attribute → assign the property as
true/false.Custom / non-standard attribute →
getAttribute/setAttribute.Application-specific metadata →
data-*+dataset.Need the initial markup value of a form field →
defaultValue,defaultChecked, or the attribute.