Skip to main content

How to Work with DOM Attributes and Properties in JavaScript

When the browser parses HTML, it creates DOM nodes for every element. Each DOM node is a JavaScript object, and like any object, it has properties. At the same time, the HTML tags that generated those nodes had attributes. These two concepts (attributes and properties) are related but not the same thing. They often overlap, sometimes stay in sync, and sometimes diverge in ways that cause confusing bugs.

Understanding the difference between HTML attributes and DOM properties is essential for writing correct DOM manipulation code. This guide explains how they relate, when they synchronize, when they do not, how to work with custom data attributes using dataset, and why property types matter more than most developers realize.

DOM Properties: Object Properties on DOM Nodes

When the browser creates a DOM element from an HTML tag, it creates a JavaScript object. That object has properties defined by its class in the DOM hierarchy. These are regular JavaScript object properties that you can read, write, and even add custom ones to.

Standard Properties Come from the Class

Each type of HTML element has a corresponding DOM class. The properties defined by that class become the DOM element's properties:

let input = document.createElement("input");

// Properties defined by HTMLInputElement
console.log(input.type); // "text" (default)
console.log(input.value); // ""
console.log(input.checked); // false
console.log(input.disabled); // false

let link = document.createElement("a");

// Properties defined by HTMLAnchorElement
console.log(link.href); // ""
console.log(link.target); // ""
console.log(link.protocol); // ":"

These properties behave like regular JavaScript object properties:

let input = document.querySelector("input");

// Read
console.log(input.value); // current value

// Write
input.value = "Hello";
console.log(input.value); // "Hello"

// Check existence
console.log("value" in input); // true
console.log("type" in input); // true

You Can Add Custom Properties to DOM Objects

Because DOM elements are regular JavaScript objects, you can attach your own properties to them. This is completely valid JavaScript, though not always the best practice:

let div = document.querySelector("div");

// Adding custom properties
div.myData = { count: 42, label: "clicks" };
div.greet = function() {
console.log("Hello from this div!");
};

console.log(div.myData.count); // 42
div.greet(); // "Hello from this div!"
caution

While adding custom properties to DOM elements works, it is generally discouraged for several reasons:

  • It can conflict with future DOM API additions (if the spec adds a property with the same name).
  • It is invisible in HTML and DevTools attribute panels.
  • It does not survive innerHTML serialization/deserialization.

For attaching custom data to elements, use data-* attributes and the dataset property (covered later in this guide).

Exploring an Element's Properties

You can see all properties of a DOM element using console.dir or by iterating:

let div = document.querySelector("div");

// See the object with all its properties
console.dir(div);

// List property names from the prototype chain
let props = [];
let obj = div;
while (obj) {
props.push(...Object.getOwnPropertyNames(obj));
obj = Object.getPrototypeOf(obj);
}
console.log(`Total properties: ${props.length}`);
// Typically 200+ properties on an HTMLDivElement

HTML Attributes: getAttribute, setAttribute, removeAttribute, hasAttribute

HTML attributes are the name-value pairs you write in your HTML markup. They exist in the HTML source and are managed through a dedicated API that is separate from DOM properties.

Reading Attributes

<a id="link" href="https://example.com" target="_blank" class="nav-link">
Example
</a>
let link = document.getElementById("link");

// getAttribute reads the HTML attribute value as a string
console.log(link.getAttribute("href")); // "https://example.com"
console.log(link.getAttribute("target")); // "_blank"
console.log(link.getAttribute("class")); // "nav-link"
console.log(link.getAttribute("id")); // "link"

// Returns null for attributes that don't exist
console.log(link.getAttribute("title")); // null
console.log(link.getAttribute("data-x")); // null

Setting Attributes

let link = document.getElementById("link");

// setAttribute sets (or creates) an HTML attribute
link.setAttribute("title", "Visit Example");
link.setAttribute("rel", "noopener");

// The attribute now appears in the HTML
console.log(link.getAttribute("title")); // "Visit Example"
console.log(link.getAttribute("rel")); // "noopener"

// You can also modify existing attributes
link.setAttribute("href", "https://newsite.com");
console.log(link.getAttribute("href")); // "https://newsite.com"

Checking for Attributes

let link = document.getElementById("link");

console.log(link.hasAttribute("href")); // true
console.log(link.hasAttribute("target")); // true
console.log(link.hasAttribute("title")); // true (we just set it)
console.log(link.hasAttribute("data-id")); // false

Removing Attributes

let link = document.getElementById("link");

link.removeAttribute("target");
console.log(link.hasAttribute("target")); // false
console.log(link.getAttribute("target")); // null

// Removing a non-existent attribute does nothing (no error)
link.removeAttribute("nonexistent"); // No error

Attribute Names Are Case-Insensitive

HTML attribute names are case-insensitive. The browser stores them in lowercase:

let div = document.querySelector("div");
div.setAttribute("MyAttribute", "hello");

// All of these return the same value
console.log(div.getAttribute("myattribute")); // "hello"
console.log(div.getAttribute("MyAttribute")); // "hello"
console.log(div.getAttribute("MYATTRIBUTE")); // "hello"

Attribute Values Are Always Strings

No matter what you set, attribute values are stored and returned as strings:

let div = document.querySelector("div");

div.setAttribute("data-count", 42);
console.log(div.getAttribute("data-count")); // "42" (string, not number)
console.log(typeof div.getAttribute("data-count")); // "string"

div.setAttribute("data-active", true);
console.log(div.getAttribute("data-active")); // "true" (string, not boolean)
console.log(typeof div.getAttribute("data-active")); // "string"

Attribute-Property Synchronization (and When It Breaks)

Here is where things get nuanced. When the browser parses HTML and creates a DOM element, it reads the HTML attributes and uses them to initialize corresponding DOM properties. For standard attributes, there is often a link between the attribute and the property, but that link is not always bidirectional, and it does not always stay in sync.

Standard Attributes Create Properties

For standard HTML attributes, the browser creates a corresponding DOM property:

<input id="name" type="text" value="Alice" class="form-input">
let input = document.getElementById("name");

// HTML attributes → DOM properties (initial sync)
console.log(input.type); // "text" (from type="text")
console.log(input.value); // "Alice" (from value="Alice")
console.log(input.className); // "form-input" (from class="form-input")
console.log(input.id); // "name" (from id="name")

Two-Way Sync for Most Properties

For most standard attributes, changes to the property update the attribute, and changes to the attribute update the property:

let input = document.getElementById("name");

// Change the property → attribute updates
input.id = "username";
console.log(input.getAttribute("id")); // "username" ✅ synced

// Change the attribute → property updates
input.setAttribute("id", "email");
console.log(input.id); // "email" ✅ synced

The value Exception: One-Way Sync

The most important exception to two-way synchronization is the input.value property. The synchronization is one-way only: attribute to property, but not property to attribute.

<input id="name" type="text" value="Alice">
let input = document.getElementById("name");

// Initially they are the same
console.log(input.value); // "Alice"
console.log(input.getAttribute("value")); // "Alice"

// Attribute → Property: works
input.setAttribute("value", "Bob");
console.log(input.value); // "Bob" ✅

// Property → Attribute: does NOT sync
input.value = "Charlie";
console.log(input.value); // "Charlie"
console.log(input.getAttribute("value")); // "Bob" ❌ still "Bob"!

This behavior exists by design. The HTML value attribute represents the initial/default value, while the DOM value property represents the current value. This lets you always retrieve the original value using getAttribute("value") or input.defaultValue, even after the user has changed the input.

let input = document.getElementById("name");

// User types "Charlie" into the input field
// or: input.value = "Charlie";

console.log(input.value); // "Charlie" (current value)
console.log(input.defaultValue); // "Alice" (original value (from HTML attribute))

// Reset the input to its original value
input.value = input.defaultValue; // Back to "Alice"

The href Attribute vs. Property

Another notable difference occurs with href. The property returns the full resolved URL, while the attribute returns exactly what was written in the HTML:

<a id="link" href="/about">About</a>
let link = document.getElementById("link");

// Property: full absolute URL
console.log(link.href); // "https://example.com/about"

// Attribute: exactly what's in the HTML
console.log(link.getAttribute("href")); // "/about"

This distinction matters when you need the raw value the developer wrote, not the browser-resolved version:

// Checking if a link is relative or absolute
let rawHref = link.getAttribute("href");

if (rawHref.startsWith("/")) {
console.log("Relative URL");
} else if (rawHref.startsWith("http")) {
console.log("Absolute URL");
} else if (rawHref.startsWith("#")) {
console.log("Hash link");
}

The class Attribute vs. className Property

The HTML attribute is named class, but the DOM property is named className (because class is a reserved word in JavaScript):

<div id="box" class="card active">Content</div>
let box = document.getElementById("box");

// Property name is className (not class)
console.log(box.className); // "card active"

// Attribute name is class (not className)
console.log(box.getAttribute("class")); // "card active"

// Both stay in sync
box.className = "card highlighted";
console.log(box.getAttribute("class")); // "card highlighted"

box.setAttribute("class", "card");
console.log(box.className); // "card"
tip

For working with CSS classes, prefer the classList API over both className and getAttribute("class"). It provides add, remove, toggle, contains, and replace methods that are safer and easier to use:

let box = document.getElementById("box");

box.classList.add("highlighted");
box.classList.remove("active");
box.classList.toggle("visible");
console.log(box.classList.contains("highlighted")); // true

Summary of Synchronization Rules

Attribute/PropertyAttribute → PropertyProperty → Attribute
idYesYes
class / classNameYesYes
hrefYes (but value differs)Yes
srcYes (but value differs)Yes
value (input)YesNo
checked (input)Only initiallyNo
selected (option)Only initiallyNo
disabledYesYes
stylePartiallyPartially

Non-Standard Attributes Have No Properties

If an HTML attribute is not recognized as a standard attribute for that element, the browser does not create a corresponding DOM property:

<div id="box" something="custom-value" data-role="admin">
Content
</div>
let box = document.getElementById("box");

// Standard attribute → property exists
console.log(box.id); // "box"

// Non-standard attribute → no property
console.log(box.something); // undefined

// But you can still access it with getAttribute
console.log(box.getAttribute("something")); // "custom-value"

This is an important distinction. getAttribute works with any attribute, standard or not. DOM properties only exist for standard attributes of that element type.

Here is a subtle aspect: an attribute might be standard for one element but non-standard for another:

// "type" is standard for <input>
let input = document.createElement("input");
input.setAttribute("type", "checkbox");
console.log(input.type); // "checkbox" (property exists)

// "type" is NOT standard for <div>
let div = document.createElement("div");
div.setAttribute("type", "checkbox");
console.log(div.type); // undefined (no property)
console.log(div.getAttribute("type")); // "checkbox" (attribute still works)

Non-Standard Attributes and dataset (data-* Attributes)

Non-standard attributes are useful for passing custom data from HTML to JavaScript or for marking elements for CSS selectors. However, using arbitrary attribute names is risky because a non-standard attribute you invent today might become a standard attribute in a future HTML version, causing conflicts.

The solution is data-* attributes. They are a standardized way to attach custom data to HTML elements, guaranteed never to conflict with future standards.

The data-* Convention

Any attribute whose name starts with data- is reserved for programmer use. The HTML specification guarantees these will never be used as standard attributes:

<article
id="post-1"
data-author="Alice"
data-category="javascript"
data-published="2024-03-15"
data-word-count="1500"
data-is-featured="true"
>
<h2>Learning JavaScript</h2>
<p>Article content...</p>
</article>

Reading data-* Attributes with getAttribute

You can always use getAttribute to read them:

let article = document.getElementById("post-1");

console.log(article.getAttribute("data-author")); // "Alice"
console.log(article.getAttribute("data-category")); // "javascript"
console.log(article.getAttribute("data-word-count")); // "1500"
console.log(article.getAttribute("data-is-featured")); // "true"

The dataset Property: A Cleaner API

Every element has a dataset property that provides a DOMStringMap of all data-* attributes. The data- prefix is stripped and the rest of the name is converted to camelCase:

let article = document.getElementById("post-1");

// data-author → dataset.author
console.log(article.dataset.author); // "Alice"

// data-category → dataset.category
console.log(article.dataset.category); // "javascript"

// data-published → dataset.published
console.log(article.dataset.published); // "2024-03-15"

// data-word-count → dataset.wordCount (hyphen → camelCase)
console.log(article.dataset.wordCount); // "1500"

// data-is-featured → dataset.isFeatured (hyphens → camelCase)
console.log(article.dataset.isFeatured); // "true"

The naming conversion rule:

  • Remove the data- prefix
  • Convert each -x to uppercase X (kebab-case to camelCase)
HTML Attributedataset Property
data-namedataset.name
data-user-iddataset.userId
data-is-activedataset.isActive
data-max-retry-countdataset.maxRetryCount

Writing data-* Attributes with dataset

The dataset property is read-write. Setting a property on dataset creates or updates the corresponding data-* attribute:

let article = document.getElementById("post-1");

// Set new data attributes
article.dataset.readTime = "5 min";
console.log(article.getAttribute("data-read-time")); // "5 min"

// Update existing data attributes
article.dataset.author = "Bob";
console.log(article.getAttribute("data-author")); // "Bob"

// Delete a data attribute
delete article.dataset.isFeatured;
console.log(article.hasAttribute("data-is-featured")); // false

data-* Values Are Always Strings

Like all HTML attributes, data-* values are strings. You need to parse them if you need other types:

let article = document.getElementById("post-1");

// ❌ These are strings, not their intended types
console.log(article.dataset.wordCount); // "1500" (string)
console.log(article.dataset.isFeatured); // "true" (string)
console.log(typeof article.dataset.wordCount); // "string"

// ✅ Parse to the correct types
let wordCount = parseInt(article.dataset.wordCount, 10); // 1500 (number)
let isFeatured = article.dataset.isFeatured === "true"; // true (boolean)

// For complex data, you can use JSON
let element = document.getElementById("config");
element.dataset.options = JSON.stringify({ theme: "dark", fontSize: 14 });

let options = JSON.parse(element.dataset.options);
console.log(options.theme); // "dark"

Using data-* Attributes in CSS

A major benefit of data-* attributes is that they can be used in CSS selectors:

/* Style elements based on data attributes */
article[data-category="javascript"] {
border-left: 4px solid yellow;
}

article[data-is-featured="true"] {
background-color: #fffde7;
}

/* Use data attributes for content */
article::before {
content: "By " attr(data-author);
font-style: italic;
color: #666;
}

Practical Use Cases for data-* Attributes

Event delegation with action identifiers:

<div id="toolbar">
<button data-action="bold">B</button>
<button data-action="italic">I</button>
<button data-action="underline">U</button>
<button data-action="link">🔗</button>
</div>
document.getElementById("toolbar").addEventListener("click", (event) => {
let button = event.target.closest("[data-action]");
if (!button) return;

let action = button.dataset.action;
console.log(`Performing action: ${action}`);

switch (action) {
case "bold": document.execCommand("bold"); break;
case "italic": document.execCommand("italic"); break;
case "underline": document.execCommand("underline"); break;
case "link": {
let url = prompt("Enter URL:");
if (url) document.execCommand("createLink", false, url);
break;
}
}
});

Configuration stored in HTML:

<div id="carousel"
data-interval="3000"
data-autoplay="true"
data-transition="fade"
data-loop="true">
<img src="slide1.jpg">
<img src="slide2.jpg">
<img src="slide3.jpg">
</div>
let carousel = document.getElementById("carousel");

let config = {
interval: parseInt(carousel.dataset.interval, 10) || 5000,
autoplay: carousel.dataset.autoplay === "true",
transition: carousel.dataset.transition || "slide",
loop: carousel.dataset.loop !== "false"
};

console.log(config);
// { interval: 3000, autoplay: true, transition: "fade", loop: true }

Tracking state on elements:

function toggleFavorite(button) {
let isFavorited = button.dataset.favorited === "true";
button.dataset.favorited = (!isFavorited).toString();
button.textContent = isFavorited ? "☆ Favorite" : "★ Favorited";

// CSS can react to this change:
// button[data-favorited="true"] { color: gold; }
}

Property Types: Strings, Booleans, Objects

While HTML attributes are always strings, DOM properties have proper JavaScript types. This is a crucial difference that affects how you work with them.

String Properties

Most DOM properties are strings, matching their attribute counterparts:

let link = document.querySelector("a");

console.log(typeof link.id); // "string"
console.log(typeof link.className); // "string"
console.log(typeof link.href); // "string"
console.log(typeof link.title); // "string"

Boolean Properties

Some DOM properties are booleans, even though the corresponding HTML attributes are either present or absent (not "true" or "false"):

<input type="checkbox" id="agree" checked disabled>
let checkbox = document.getElementById("agree");

// DOM properties are booleans
console.log(checkbox.checked); // true
console.log(checkbox.disabled); // true
console.log(typeof checkbox.checked); // "boolean"
console.log(typeof checkbox.disabled); // "boolean"

// HTML attributes are strings (or null)
console.log(checkbox.getAttribute("checked")); // "" (empty string, attribute exists)
console.log(checkbox.getAttribute("disabled")); // "" (empty string, attribute exists)

This difference matters enormously when setting values:

let checkbox = document.getElementById("agree");

// ✅ Correct: use the boolean property
checkbox.checked = false;
console.log(checkbox.checked); // false

// ❌ Incorrect: setting the attribute to "false" does NOT uncheck it
checkbox.setAttribute("checked", "false");
console.log(checkbox.checked); // true!
// The attribute "checked" EXISTS (even with value "false"), so the checkbox stays checked

This is one of the most confusing behaviors in the DOM. For boolean HTML attributes, the presence of the attribute means true, regardless of its value. Setting checked="false" in HTML still makes the checkbox checked. The only way to "uncheck" via attributes is to remove the attribute entirely:

// ✅ To uncheck via attributes, REMOVE the attribute
checkbox.removeAttribute("checked");

// ✅ But using the property is much simpler and correct
checkbox.checked = false;

Common boolean attribute/property pairs:

HTML AttributeDOM PropertyType
checkedcheckedboolean
disableddisabledboolean
readonlyreadOnlyboolean
requiredrequiredboolean
hiddenhiddenboolean
selectedselectedboolean
multiplemultipleboolean
autofocusautofocusboolean
autoplayautoplayboolean

Object Properties

Some DOM properties return objects rather than primitive values:

let div = document.querySelector("div");

// style is a CSSStyleDeclaration object
console.log(typeof div.style); // "object"
console.log(div.style instanceof CSSStyleDeclaration); // true

div.style.color = "red";
div.style.fontSize = "18px";

// getAttribute returns the SERIALIZED string
console.log(div.getAttribute("style")); // "color: red; font-size: 18px;"
// The property is a rich object, the attribute is a flat string
let input = document.querySelector('input[list="options"]');

// The list property returns the actual <datalist> element (an object)
console.log(input.list); // <datalist id="options">...</datalist>

// The attribute returns the string ID
console.log(input.getAttribute("list")); // "options"

Attribute vs. Property Decision Guide

SituationUse PropertyUse Attribute
Read/write the current value of an inputinput.valueNo
Read/write a boolean state (checked, disabled)input.checked = trueNo
Get the initial/default value from HTMLNogetAttribute("value")
Get the raw href as written in HTMLNogetAttribute("href")
Work with non-standard or data-* attributesdataset.xgetAttribute("data-x")
Read/write standard attributes on known elementsProperty (usually)Either works
Work with SVG or XML attributesNogetAttribute
tip

As a general rule: use DOM properties for standard attributes on HTML elements. They have correct types (booleans, numbers, objects) and represent the current state. Use getAttribute / setAttribute when you need the raw HTML attribute value, when working with non-standard attributes, or when dealing with SVG/XML elements.

The attributes Collection

Every element has an attributes property that returns a live NamedNodeMap of all its attributes. This collection provides a way to iterate over all attributes on an element.

Reading All Attributes

<a id="link"
href="https://example.com"
target="_blank"
class="nav-link"
data-section="header"
title="Example Site">
Example
</a>
let link = document.getElementById("link");
let attrs = link.attributes;

console.log(attrs.length); // 5

// Iterate over all attributes
for (let attr of attrs) {
console.log(`${attr.name} = "${attr.value}"`);
}
// id = "link"
// href = "https://example.com"
// target = "_blank"
// class = "nav-link"
// data-section = "header"
// title = "Example Site"

Attr Objects

Each item in the attributes collection is an Attr object with name and value properties:

let link = document.getElementById("link");

let hrefAttr = link.attributes[1]; // Second attribute (href)
console.log(hrefAttr.name); // "href"
console.log(hrefAttr.value); // "https://example.com"

// Access by name
let classAttr = link.attributes.getNamedItem("class");
console.log(classAttr.name); // "class"
console.log(classAttr.value); // "nav-link"

// Shorthand: access by name directly
console.log(link.attributes["class"].value); // "nav-link"

Practical Use: Cloning Attributes

The attributes collection is useful when you need to copy all attributes from one element to another:

function copyAttributes(source, target) {
for (let attr of source.attributes) {
target.setAttribute(attr.name, attr.value);
}
}

let original = document.getElementById("link");
let clone = document.createElement("a");
clone.textContent = "Cloned Link";
copyAttributes(original, clone);

console.log(clone.outerHTML);
// <a id="link" href="https://example.com" target="_blank" class="nav-link" ...>Cloned Link</a>

Practical Use: Serializing Element Configuration

function getElementConfig(element) {
let config = {};
for (let attr of element.attributes) {
config[attr.name] = attr.value;
}
return config;
}

let link = document.getElementById("link");
console.log(getElementConfig(link));
// {
// id: "link",
// href: "https://example.com",
// target: "_blank",
// class: "nav-link",
// "data-section": "header",
// title: "Example Site"
// }

Live Nature of attributes

The attributes collection is live. It updates when attributes are added or removed:

let div = document.querySelector("div");
let attrs = div.attributes;

console.log(attrs.length); // e.g., 2

div.setAttribute("data-new", "value");
console.log(attrs.length); // 3 (updated automatically)

div.removeAttribute("data-new");
console.log(attrs.length); // 2 (updated automatically)

Complete Practical Example

Here is a comprehensive example that demonstrates attributes, properties, dataset, and their interactions:

<!DOCTYPE html>
<html>
<head>
<title>Attributes and Properties Demo</title>
<style>
.card { border: 1px solid #ddd; padding: 16px; margin: 8px; border-radius: 8px; }
.card[data-priority="high"] { border-color: red; }
.card[data-priority="medium"] { border-color: orange; }
.card[data-priority="low"] { border-color: green; }
.card[data-completed="true"] { opacity: 0.5; text-decoration: line-through; }
</style>
</head>
<body>
<div id="app">
<h1>Task Board</h1>

<div id="tasks">
<div class="card"
data-task-id="1"
data-priority="high"
data-completed="false"
data-created="2024-03-15">
<h3>Fix login bug</h3>
<label>
<input type="checkbox" class="complete-toggle"> Done
</label>
</div>

<div class="card"
data-task-id="2"
data-priority="medium"
data-completed="false"
data-created="2024-03-16">
<h3>Update documentation</h3>
<label>
<input type="checkbox" class="complete-toggle"> Done
</label>
</div>

<div class="card"
data-task-id="3"
data-priority="low"
data-completed="false"
data-created="2024-03-17">
<h3>Refactor utils module</h3>
<label>
<input type="checkbox" class="complete-toggle"> Done
</label>
</div>
</div>

<div id="info"></div>
</div>

<script>
let tasksContainer = document.getElementById("tasks");

// Event delegation using data attributes
tasksContainer.addEventListener("change", (event) => {
if (!event.target.matches(".complete-toggle")) return;

let card = event.target.closest(".card");

// Use the boolean PROPERTY to get the current checkbox state
let isCompleted = event.target.checked; // boolean (not string!)

// Use dataset to update the data attribute
card.dataset.completed = isCompleted.toString();

// CSS automatically updates (opacity, line-through)
// because the data-completed attribute changed

logTaskInfo(card);
});

function logTaskInfo(card) {
let checkbox = card.querySelector(".complete-toggle");

console.log("=== Task Info ===");

// dataset properties (strings from data-* attributes)
console.log("Task ID:", card.dataset.taskId); // "1" (string)
console.log("Priority:", card.dataset.priority); // "high" (string)
console.log("Completed:", card.dataset.completed); // "true" (string!)
console.log("Created:", card.dataset.created); // "2024-03-15" (string)

// DOM property (proper boolean)
console.log("Checkbox checked:", checkbox.checked); // true (boolean!)

// Attribute vs Property difference
console.log("Attribute 'checked':", checkbox.getAttribute("checked")); // null (never set in HTML)
console.log("Property checked:", checkbox.checked); // true

// All attributes on the card
console.log("All attributes:");
for (let attr of card.attributes) {
console.log(` ${attr.name} = "${attr.value}"`);
}
}

// Demonstrate attribute vs property for href
function showAttributePropertyDifference() {
let link = document.createElement("a");
link.setAttribute("href", "/about");
document.body.appendChild(link);

let info = document.getElementById("info");
info.innerHTML = `
<h3>Attribute vs Property</h3>
<p><strong>getAttribute("href"):</strong> ${link.getAttribute("href")}</p>
<p><strong>link.href (property):</strong> ${link.href}</p>
<p>The attribute gives the raw value. The property gives the resolved URL.</p>
`;

link.remove();
}

showAttributePropertyDifference();
</script>
</body>
</html>

This example demonstrates:

  • Boolean properties (checkbox.checked) versus string attributes (getAttribute("checked"))
  • dataset for reading and writing data-* attributes with camelCase conversion
  • CSS responding to data-* attribute changes without any class toggling
  • Event delegation using closest and matches with data attributes
  • The attribute vs. property difference for href

Summary

HTML attributes and DOM properties are two related but distinct systems for working with element data.

HTML Attributes:

  • Written in HTML markup
  • Accessed via getAttribute, setAttribute, removeAttribute, hasAttribute
  • Names are case-insensitive
  • Values are always strings
  • Represent the initial state from the HTML source
  • Work with any attribute (standard and non-standard)
  • The attributes collection lists all attributes on an element

DOM Properties:

  • JavaScript object properties on DOM nodes
  • Accessed via dot notation (element.id, element.value)
  • Names are case-sensitive
  • Values have proper types (strings, booleans, objects, numbers)
  • Represent the current state of the element
  • Only exist for standard attributes of that element type

Synchronization:

  • Most standard attributes sync bidirectionally with their properties
  • value, checked, and selected sync only from attribute to property (one-way)
  • href and src properties return resolved URLs, while attributes return raw values
  • Non-standard attributes have no corresponding properties

data-* Attributes and dataset:

  • data-* attributes are the standard way to attach custom data to elements
  • Access via element.dataset.camelCaseName
  • Hyphens in attribute names convert to camelCase in dataset
  • Values are always strings (parse manually for other types)
  • Usable in CSS selectors for styling
  • Guaranteed to never conflict with future HTML standards

Key Rules:

  • Use properties for standard attributes (correct types, current state)
  • Use getAttribute for raw attribute values, non-standard attributes, or SVG
  • Use dataset for custom data instead of inventing non-standard attributes
  • For boolean attributes (checked, disabled), always use the property, never setAttribute("checked", "false")