Attributes and properties

The browser reads your HTML (the technical word is parses it) and builds a tree of DOM objects from it. For element nodes, most standard HTML attributes turn into properties on those objects automatically.

Write <body id="page"> and the DOM object for the body ends up with body.id === "page". Feels like the attribute is the property. It isn’t, and the difference matters more than it first appears.

The mapping between attributes and properties is not one-to-one. Some attributes have no property. Some properties have no attribute. Some share a name but hold different types, or different values, or sync in only one direction. This chapter pulls the two apart so you know which one you’re touching and why.

HTML attributes (the markup)id = “page”always a stringname is case-insensitiveDOM properties (the object)elem.id = “page”any type (string, bool, object…)name is case-sensitiveparse
Two separate stores. The HTML attribute lives on the parsed markup; the DOM property lives on the JavaScript object. Standard attributes create a bridge between them.

DOM properties

You’ve already met plenty of built-in DOM properties. There are a lot of them. Nothing stops you from adding your own, though, because DOM nodes are ordinary JavaScript objects.

Set a new property on document.body and it just sticks, the same way it would on any object:

document.body.pageMeta = {
  author: 'Maya',
  role: 'editor'
};

alert(document.body.pageMeta.role); // editor

Methods work too:

document.body.showTag = function() {
  alert(this.tagName);
};

document.body.showTag(); // BODY — "this" inside the method is document.body

You can go a level deeper and add to a built-in prototype like Element.prototype, which puts your method on every element at once:

Element.prototype.greet = function() {
  alert(`Hi, I'm a ${this.tagName} element`);
};

document.documentElement.greet(); // Hi, I'm a HTML element
document.body.greet();            // Hi, I'm a BODY element

So DOM properties and methods behave like regular JavaScript object members:

  • They can hold any value.
  • They are case-sensitive: elem.nodeType works, elem.NoDeTyPe is just an unrelated undefined property.

HTML attributes

Attributes are the pieces you write inside a tag. When the browser parses HTML, it recognizes standard attributes and builds matching DOM properties from them. Hand it something it doesn’t recognize, and no property appears.

<body id="menu" flavor="citrus">
  <script>
    alert(document.body.id); // menu
    // a non-standard attribute creates no property
    alert(document.body.flavor); // undefined
  </script>
</body>

“Standard” is judged per element type, not globally. An attribute that’s standard on one element can be unknown on another. type is standard on <input> (defined in HTMLInputElement) but meaningless on <body> (defined in HTMLBodyElement). Each element class has its own list in the spec.

<body id="body" type="...">
  <input id="input" type="text">
  <script>
    alert(input.type); // text
    alert(body.type);  // undefined — not standard for <body>, so no property
  </script>
</body>
<input type=“text”>attr type=“text”input.type = “text”<body type=“…”>attr type=“…”body.type = undefined
Whether an attribute becomes a property depends on the element. type is standard on <input>, so it maps; on <body> it stays attribute-only.

If an attribute is non-standard, there’s no property for it. So how do you reach it? Through a set of methods that work with attributes directly, no matter whether they’re standard:

  • elem.hasAttribute(name) — does the attribute exist?
  • elem.getAttribute(name) — read its value.
  • elem.setAttribute(name, value) — set its value.
  • elem.removeAttribute(name) — delete it.

These operate on exactly what’s written in the HTML.

There’s also elem.attributes, a collection of objects from the built-in Attr class, each with a name and value. It includes every attribute, standard or not.

Reading a non-standard attribute:

<body flavor="citrus">
  <script>
    alert(document.body.getAttribute('flavor')); // citrus
  </script>
</body>

Two features of HTML attributes are worth burning into memory:

  • Names are case-insensitive. id and ID refer to the same attribute.
  • Values are always strings. Whatever you set, it’s stored as text.

A fuller run-through:

<body>
  <div id="elem" mascot="Otter"></div>

  <script>
    alert( elem.getAttribute('Mascot') ); // (1) 'Otter', reading

    elem.setAttribute('Extra', 123); // (2) writing

    alert( elem.outerHTML ); // (3) is the attribute in the HTML? yes

    for (let attr of elem.attributes) { // (4) list every attribute
      alert( `${attr.name} = ${attr.value}` );
    }
  </script>
</body>

What each numbered line shows:

  1. getAttribute('Mascot') uses a capital M while the HTML wrote mascot. Doesn’t matter — attribute names ignore case.
  2. You passed the number 123, but it lands in the attribute as the string "123".
  3. Everything you set, including custom attributes, shows up in outerHTML.
  4. attributes is iterable and carries every attribute, standard and non-standard alike, as {name, value} objects.

Property-attribute synchronization

When a standard attribute changes, its property updates automatically, and in most cases the reverse holds too. Change one side and the other follows.

Here id is edited as an attribute, then as a property, and each change is visible from the other side:

<input>

<script>
  let input = document.querySelector('input');

  // attribute => property
  input.setAttribute('id', 'id');
  alert(input.id); // id (updated)

  // property => attribute
  input.id = 'newId';
  alert(input.getAttribute('id')); // newId (updated)
</script>

But sync isn’t always two-way. input.value is the classic exception: the attribute flows into the property, never back out.

<input>

<script>
  let input = document.querySelector('input');

  // attribute => property
  input.setAttribute('value', 'draft');
  alert(input.value); // draft

  // NOT property => attribute
  input.value = 'edited';
  alert(input.getAttribute('value')); // draft (attribute unchanged!)
</script>

Setting the attribute updates the property. Setting the property leaves the attribute alone.

id (two-way)attribute idproperty .idvalue (one-way)attribute valueproperty .valueproperty → attribute: blocked
id syncs both ways. value only syncs attribute → property; a property change does not write back to the attribute.

That one-way behavior is useful, not just a quirk. The user types into a field and input.value changes with every keystroke. If you later want the original value the page shipped with, it’s still sitting untouched in the attribute — input.getAttribute('value').

Type in the field below and watch the two readings drift apart. The property tracks every keystroke; the attribute stays frozen at the value the HTML shipped with.

interactivevalue: property changes, attribute doesn't

DOM properties are typed

Attributes are always strings. Properties are not. That gap is where a lot of the attribute-vs-property distinction earns its keep.

For a checkbox, input.checked is a boolean, while the attribute is just the string that was written (an empty string here):

<input id="input" type="checkbox" checked> checkbox

<script>
  alert(input.getAttribute('checked')); // "" (empty string)
  alert(input.checked);                 // true (a boolean)
</script>

The style attribute is a string. The style property is a live CSSStyleDeclaration object you can read and set field by field:

<div id="div" style="color:teal;font-size:140%">Welcome</div>

<script>
  // attribute: a plain string
  alert(div.getAttribute('style')); // color:teal;font-size:140%

  // property: an object
  alert(div.style);       // [object CSSStyleDeclaration]
  alert(div.style.color); // teal
</script>
getAttribute(‘checked’)
“”  (string)
.checked
true  (boolean)
getAttribute(‘style’)
“color:teal;…”  (string)
.style
CSSStyleDeclaration  (object)
Same name, different types. The attribute stores raw text; the property is parsed into a typed value or object.

Toggle the checkbox and watch the two values. The property flips between the booleans true and false; the attribute reports only whether it was present in the original HTML, and never changes as you click.

interactivechecked: boolean property vs string attribute

Most properties are strings, so these type differences are the exception. But even a string property can differ in value from its attribute. href is the well-known case: the DOM property is always a fully resolved URL, even when the attribute holds a relative path or a bare #hash.

<a id="a" href="#intro">link</a>
<script>
  // attribute: exactly as written
  alert(a.getAttribute('href')); // #intro

  // property: resolved to an absolute URL
  alert(a.href); // full URL like http://example.com/page#intro
</script>

When you need the value exactly as authored in the HTML, reach for getAttribute. When you want the browser’s processed version, use the property.

Non-standard attributes, dataset

Standard attributes cover most of what you write. But sometimes you want a custom attribute — to carry data from HTML into JavaScript, or to tag elements so your script can find them.

Here two divs are marked with a custom bind-field attribute, and a loop fills each one from a user object:

<!-- mark this div to show "name" -->
<div bind-field="name"></div>
<!-- and this one to show "age" -->
<div bind-field="age"></div>

<script>
  let user = {
    name: "Raj",
    age: 34
  };

  for(let div of document.querySelectorAll('[bind-field]')) {
    // read the mark, insert the matching field
    let field = div.getAttribute('bind-field');
    div.innerHTML = user[field]; // "Raj" into name, 34 into age
  }
</script>

Custom attributes are also handy for styling. CSS can select on them, so you can drive appearance from a single attribute value:

<style>
  /* styles keyed off the custom "task-priority" attribute */
  .task[task-priority="low"]    { color: green; }
  .task[task-priority="medium"] { color: blue;  }
  .task[task-priority="high"]   { color: red;   }
</style>

<div class="task" task-priority="low">A low priority task.</div>
<div class="task" task-priority="medium">A medium priority task.</div>
<div class="task" task-priority="high">A high priority task.</div>

Why prefer an attribute over classes like .task-priority-low, .task-priority-medium, .task-priority-high? Because switching state is a single assignment instead of a remove-old-class / add-new-class dance:

// simpler than juggling classes
div.setAttribute('task-priority', 'high');

There’s a catch with inventing your own attribute names. HTML keeps evolving — new attributes get standardized over time to meet real developer needs. If you pick a plain name today and the spec later gives that name a meaning, your page could start behaving in surprising ways.

To sidestep that, HTML reserves the whole data-* space for you.

Every attribute whose name starts with data- is set aside for developers, and all of them show up on the dataset property.

An element with data-planet is readable as elem.dataset.planet:

<body data-planet="Mars">
<script>
  alert(document.body.dataset.planet); // Mars
</script>

Multi-word names are camel-cased on the way in: data-task-priority becomes dataset.taskPriority.

data-task-priority
dataset.taskPriority
Naming translation: hyphenated data-* attribute names become camelCase keys on dataset.

The task-priority example, rewritten the safe way:

<style>
  .task[data-task-priority="low"]    { color: green; }
  .task[data-task-priority="medium"] { color: blue;  }
  .task[data-task-priority="high"]   { color: red;   }
</style>

<div id="task" class="task" data-task-priority="low">
  A low priority task.
</div>

<script>
  // read
  alert(task.dataset.taskPriority); // low

  // modify
  task.dataset.taskPriority = "medium"; // (*)
</script>

data-* attributes are the valid, future-proof way to attach custom data to elements.

And dataset is a two-way street: line (*) writes back to the data-task-priority attribute, so the CSS selector re-matches and the color flips to blue. No manual re-styling needed.

Cycle the state below with the button. The only JavaScript is a single dataset.taskPriority = ... assignment — the color change comes entirely from the CSS selector re-matching the updated attribute.

interactiveWriting dataset re-triggers the CSS

Summary

  • Attributes are what you write in HTML.
  • Properties are what live on the DOM object.

Side by side:

Properties Attributes
Type Any value; standard ones have types defined in the spec Always a string
Name Case-sensitive Case-insensitive

The attribute methods:

  • elem.hasAttribute(name) — check existence.
  • elem.getAttribute(name) — read the value.
  • elem.setAttribute(name, value) — write the value.
  • elem.removeAttribute(name) — delete it.
  • elem.attributes — the collection of all attributes.

Reach for properties by default; they’re typed and usually what you want. Fall back to attributes only when a property won’t do:

  • You need a non-standard attribute — but if it begins with data-, use dataset instead.
  • You need the value exactly as written in HTML, because the property may differ. href, for one, resolves to a full URL while the attribute keeps the original text.