Skip to content
JavaScript js dom 4 min read

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). Use getAttribute only when you specifically want the original markup value or a non-reflected custom attribute.

You want…Use
The current value the user seesel.value (property)
Whether a box is checked right nowel.checked (property)
The original markup valueel.getAttribute("value")
A custom/non-standard attributegetAttribute / 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:

  • getAttribute returns null when the attribute is missing (not undefined, not "").
  • setAttribute always 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; use getAttribute only 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 via dataset, never in invented non-standard attributes.
  • Remember every attribute and dataset value is a string; convert with Number() or compare against "true" explicitly.
  • Use removeAttribute (or delete el.dataset.x) to delete an attribute; setting it to "" leaves it present and truthy for booleans.
  • Check existence with hasAttribute rather than relying on getAttribute(...) !== null when intent matters.
Last updated June 1, 2026
Was this helpful?