JavaScriptAttributes & Properties

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

JS
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, most data-*) 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

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

JS
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 there

text input: same idea with value

JS
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 attribute

href: attribute is raw, property is resolved

JS
const a = document.getElementById("link");

a.getAttribute("href"); // "/about"
a.href;                 // "https://example.com/about" — fully resolved URL
Rule of thumb
If you want **what the user can see right now** — the live state — use the property (`el.checked`, `el.value`). If you want **what was in the HTML originally** — and you don't mind the string form — use the attribute. They differ on purpose for form fields, so the browser can show a reset state.
Boolean 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.

HTML
<input disabled>
<input disabled="false">      <!-- still disabled! -->
<input disabled="disabled">   <!-- old-school but valid -->

JS
input.disabled = true;          // sets it
input.disabled = false;         // unsets it
input.removeAttribute("disabled"); // also unsets it
input.setAttribute("disabled", "false"); // STILL disabled (presence wins)
Don't try to disable by setting a string
A common bug is `el.setAttribute("disabled", false)`. That sets the attribute value to the string `"false"` and the element stays disabled. Either set the **property** (`el.disabled = false`) or **remove the attribute**.
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.

HTML
<li class="task" data-id="42" data-user-name="ada">Buy milk</li>

JS
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 attribute
  • Values 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

JS
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.

JS
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 attributegetAttribute / setAttribute.

  • Application-specific metadatadata-* + dataset.

  • Need the initial markup value of a form field → defaultValue, defaultChecked, or the attribute.