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.
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.nodeTypeworks,elem.NoDeTyPeis 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>
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.
idandIDrefer 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:
getAttribute('Mascot')uses a capital M while the HTML wrotemascot. Doesn’t matter — attribute names ignore case.- You passed the number
123, but it lands in the attribute as the string"123". - Everything you set, including custom attributes, shows up in
outerHTML. attributesis 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.
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.
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>
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.
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.
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.
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-, usedatasetinstead. - 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.