Attributes, properties and the data cache
attr versus prop versus data, why the data cache surprises people, and how to read form values without guessing.
attr and prop are not interchangeable
// Attributes are the HTML as written. Properties are the live DOM state.
const checkbox = document.getElementById('terms');
console.log(checkbox.getAttribute('checked')); // "" once set in the markup, never changes
console.log(checkbox.checked); // true/false, follows the user
$('#terms').attr('checked'); // reads the attribute β reflects the initial markup
$('#terms').prop('checked'); // reads the property β reflects reality
// The rule of thumb: prop for state booleans, attr for HTML that is genuinely markup.
$('#terms').prop('checked', true); // check it
$('#terms').prop('disabled', false); // enable it
$('#link').attr('href', '/next'); // change the destination
$('#img').attr('alt', 'Product photo'); // change an attribute
$('#field').attr('data-state', 'dirty'); // custom data attributes go through attr| Property | Use | Why not the other |
|---|---|---|
checked | prop | The attribute is only the default state |
disabled | prop | Setting the attribute is inconsistent across browsers |
selected | prop | Same as checked |
value | .val() | The property holds what the user typed |
href | attr | The property is the resolved absolute URL |
class | addClass/removeClass | Never write the string by hand |
data-* | attr or data | See the cache below |
π‘
The old advice "use prop for booleans" is still correct, but the deeper reason is worth knowing:
attr() maps to getAttribute, which returns the markup default, while prop() maps to the live DOM property the browser actually uses to render and submit.The data cache
// .data() reads data-* attributes ONCE, then caches. After that it is
// a private store: writing with .data() does NOT touch the attribute.
const $el = $('#cart');
// markup: <div id="cart" data-count="3" data-options='{"gift":true}'></div>
$el.data('count'); // 3 (number β jQuery converts the string)
$el.data('options'); // { gift: true } (parsed JSON)
$el.attr('data-count'); // "3" (the raw string)
$el.data('count', 4); // writes to the CACHE only
$el.attr('data-count'); // still "3" β the attribute is unchanged
$el.data('count'); // 4
// To write through to the attribute you must set it explicitly.
$el.attr('data-count', 4);
// Later markup changes are invisible to the cache.
$el.attr('data-count', 99);
$el.data('count'); // still 4 β the cache wins
// .removeData() clears one key; it does not touch the attribute.
$el.removeData('count');| Expression | Reads from | Writes to |
|---|---|---|
.data('k') | Cache, seeded from the attribute | β |
.data('k', v) | β | Cache only |
.attr('data-k') | Attribute | Attribute |
.attr('data-k', v) | β | Attribute |
.removeData('k') | β | Cache (cleared) |
.removeAttr('data-k') | β | Attribute |
// The trap in practice: two components disagreeing about the same value.
function cartCount() {
return $('#cart').data('count'); // reads the cache
}
function serverSays(n) {
$('#cart').attr('data-count', n); // writes the attribute
}
serverSays(7);
console.log(cartCount()); // 3 or 4 β NOT 7
// Pick one source of truth per value and stay with it:
// server-rendered values -> .attr('data-x') on read and write
// client-only state -> .data('x') and never look at the attribute
// anything real -> a plain variable or a store, with the DOM as a render targetClass and form value helpers
$('#panel').addClass('is-open');
$('#panel').removeClass('is-open');
$('#panel').toggleClass('is-open', shouldOpen); // second arg forces a state
$('#panel').hasClass('is-open'); // boolean
$('#panel').toggleClass('is-open is-dirty'); // multiple at once
// Reading form state correctly
$('#terms').is(':checked'); // boolean property
$('#plan').val(); // selected option's value
$('#plan option:selected').text(); // its label
$('input[name="tags"]:checked') // a checkbox group
.map((_, el) => el.value)
.get(); // ['red', 'blue']
// prop on a collection applies to every element and returns the collection
$('.js-select-all').prop('checked', true);
// Attributes for things assistive technology reads
$('#accordion-trigger').attr('aria-expanded', 'true');
$('#dialog').attr('aria-hidden', 'false');
$('#live-region').text('Saved.');.map()on a jQuery collection returns a jQuery-wrapped array;.get()unwraps it into a plain JavaScript array..toggleClass(name, state)with a boolean is the safe form. Without it, calling toggle twice from two sources produces a class that flickers.- Use
.prop()foraria-*? No β ARIA attributes are attributes, so.attr()is correct there. State booleans on the element itself useprop. - The
classattribute is the intersection of markup and behaviour. Choose one owner β usually a framework or a component class β and let everything else toggle through it.
FAQ
Why does .data() return an old value?
It caches the attribute on first read. Changing the attribute afterwards does not refresh the cache. Either read the attribute directly with
.attr(), or clear the cache with .removeData() before reading again.Should I use .data() at all?
Only for values the DOM owns and nobody else writes. For application state, a plain object or a store is clearer, and the DOM becomes a render target rather than a second source of truth.
Related
DOM manipulation, insertion and cloning Form handling, serialization and validation
Last refreshed 2026-09-18.