Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

jQuery 4 (legacy code and migration to DOM APIs)  Module 2 – Selecting, reading and changing the page

attr(), prop(), classes and values in jQuery 4

Why checkboxes need .prop(), what .attr() returns and stores for boolean attributes in jQuery 4, and how attr(name, false) and toggleClass() changed.

  • Intermediate
  • 15 minutes
  • Examples run with Node.js 24.21.0, jQuery 4.0.0 and quickjs 0.32.0
  • By MySmartCoPilot
Free lesson, Pro answers: 15 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Choose between .attr(), .prop() and .val() for checked, selected, disabled and value
  • Predict what .attr() returns and stores for boolean attributes in jQuery 3 and 4, and fix code that relies on it
  • Find code that relies on .attr(name, false) or on toggleClass() without a class name, and rewrite it for jQuery 4
  • Use jQuery Migrate to report these changes, and translate the code to classList, toggleAttribute() and properties

Before you start

What you’ll learn · Before you start

What you will learn

  • Choose between .attr(), .prop() and .val() for checked, selected, disabled and value
  • Predict what .attr() returns and stores for boolean attributes in jQuery 3 and 4, and fix code that relies on it
  • Find code that relies on .attr(name, false) or on toggleClass() without a class name, and rewrite it for jQuery 4
  • Use jQuery Migrate to report these changes, and translate the code to classList, toggleAttribute() and properties

Before you start

On this page

On jQuery 3.7.1, $('#news').attr('checked') returns "checked" for <input id="news" checked>; on jQuery 4.0.0 it returns "", so if ($('#news').attr('checked')) quietly stops running. Four attribute and class calls changed meaning in jQuery 4, none of them with an error, and all four hide in settings pages and form code of every age.

The settings form of this lesson runs in jsdom on both versions, with and without jQuery Migrate, and each run logs what every call returned or stored. You will keep the HTML’s defaults apart from the live state of a form, fix code that reads or writes boolean attributes with .attr(), find the silent changes to .attr(name, false) and toggleClass(), let Migrate point at every affected line, and write the native code that says what it means.

Attributes hold the HTML defaults; properties hold the live state

An attribute is what the HTML says: checked, value="411001", selected. A DOM property: A value on the JavaScript object of an element, such as el.checked or el.value. The browser sets it from the HTML attribute when the page loads; after that, clicks and typing change the property and leave the attribute as the default. is what the form holds now: el.checked, el.value. When the page loads, the browser sets each property from its attribute; from then on, a click or a keystroke changes the property alone. jQuery’s documentation says the most important thing to remember about the checked attribute is that “it does not correspond to the checked property” (.prop()). Step through the newsletter box of the settings form:

Interactive · step by step The checked attribute and the checked property of one box Step through the newsletter box from page load to a reset of the form, and compare what each side says.
The checked attribute and the checked property of one box, step 5 of 5. Showing <input checked>, checked attribute, checked property (true again), A click, .attr('checked') (4.0: "", 3.7: "checked"), .prop('checked') (returns false), form.reset(). In focus: form.reset(), checked attribute, checked property.sets it at load<input checked>in the HTMLchecked attributepresent: ""checked propertytrue againA click.attr('checked')4.0: "", 3.7: "checked".prop('checked')returns falseform.reset()

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. The page loads. The HTML says checked, so the box has a checked attribute with an empty value, and its checked property starts as true.
  2. The user unticks it. A click changes only the property, which is now false. The attribute still says checked: it is the default, not the state.
  3. Reading the attribute. .attr('checked') reads the attribute, so it does not see the click. jQuery 4 returns its real value, an empty string; jQuery 3 returned "checked".
  4. Reading the property. .prop('checked') and the DOM's el.checked read the property, so both say false, as the user sees it.
  5. The attribute is the default. form.reset() sets the property back from the attribute: the box is ticked again. The property el.defaultChecked is the attribute as a boolean.

The form’s script reads the same box, the select, the PIN field and the Save button before and after changes like a user’s:

The settings form and state.js

prefs/state.html

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Notification settings</title>
  </head>
  <body>
    <form id="prefs">
      <label><input type="checkbox" id="news" name="news" checked> Newsletter</label>
      <label><input type="checkbox" id="sms" name="sms" checked="CHECKED"> SMS alerts</label>
      <label><input type="checkbox" id="calls" name="calls"> Phone calls</label>
      <select id="lang" name="lang">
        <option value="en">English</option>
        <option value="hi" selected>Hindi</option>
        <option value="ta">Tamil</option>
      </select>
      <input type="text" id="pin" name="pin" value="411001">
      <button id="save" type="submit" disabled>Save</button>
    </form>
    <a id="help" href="/help/" title="Help with settings">Help</a>
    <div id="panel" class="card open" aria-hidden="true" data-mode="edit">Your settings</div>
    <script src="https://code.jquery.com/jquery-4.0.0.min.js" integrity="sha256-OaVG6prZf4v69dPg6PhVattBXkcOWQB62pdZ3ORyrao=" crossorigin="anonymous"></script>
    <script src="state.js"></script>
  </body>
</html>

prefs/state.js

// state.js: what the HTML says (attributes) and what the form holds now (properties), before and after the user
// changes it. click() changes a checkbox as a user's click does.
const news = document.getElementById('news');
const calls = document.getElementById('calls');
const q = (value) => JSON.stringify(value);

console.log(`news on load: attribute ${q(news.getAttribute('checked'))}, .prop('checked') ${$(news).prop('checked')}`);
const callsValueUnticked = $('#calls').val();
news.click();
calls.click();
console.log(`news after a click: attribute ${q(news.getAttribute('checked'))}, .prop('checked') ${$(news).prop('checked')}, el.checked ${news.checked}`);
console.log(`calls after a click: attribute ${q(calls.getAttribute('checked'))}, .prop('checked') ${$(calls).prop('checked')}`);
console.log(`news.defaultChecked (the attribute as a boolean): ${news.defaultChecked}`);
console.log(`$('#calls').val(), a box with no value attribute: ${q(callsValueUnticked)} unticked, ${q($('#calls').val())} ticked`);

$('#lang').val('ta');
const hindi = $('#lang option[value=hi]');
console.log(`select after .val('ta'): .val() ${q($('#lang').val())}, Hindi .prop('selected') ${hindi.prop('selected')}, Hindi still has the attribute: ${hindi[0].hasAttribute('selected')}`);

document.getElementById('pin').value = '560001';
console.log(`pin after typing: .val() ${q($('#pin').val())}, .attr('value') ${q($('#pin').attr('value'))}, .prop('defaultValue') ${q($('#pin').prop('defaultValue'))}`);

$('#save').prop('disabled', false);
console.log(`save after .prop('disabled', false): the disabled attribute is gone: ${!document.getElementById('save').hasAttribute('disabled')}`);

document.getElementById('prefs').reset();
console.log(`after form.reset(): news ${news.checked}, calls ${calls.checked}, pin ${q($('#pin').val())}, language ${q($('#lang').val())}`);
What state.js logged JavaScript · state.mjs
// Prints what prefs/state.js logged in jsdom (recorded.mjs).
import { show } from './show.mjs';

show('prefs/state.html');

Output

news on load: attribute "", .prop('checked') true
news after a click: attribute "", .prop('checked') false, el.checked false
calls after a click: attribute null, .prop('checked') true
news.defaultChecked (the attribute as a boolean): true
$('#calls').val(), a box with no value attribute: "on" unticked, "on" ticked
select after .val('ta'): .val() "ta", Hindi .prop('selected') false, Hindi still has the attribute: true
pin after typing: .val() "560001", .attr('value') "411001", .prop('defaultValue') "411001"
save after .prop('disabled', false): the disabled attribute is gone: true
after form.reset(): news true, calls false, pin "411001", language "hi"
(jQuery 3.7.1 logged the same 9 lines)

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node state.mjs

Every pair tells the same story: after the click the checked attribute was still there and .prop('checked') said false; after typing, .attr('value') still said 411001 and .val() said 560001. The attribute is the default, which is also why form.reset() brought every field back to it. Two lines catch people out: .val() of a checkbox is the value it sends, "on" when the HTML gives none, unticked or ticked, never whether it is ticked; and .prop('disabled', false) removed the disabled attribute too, because for disabled the property and the attribute are kept in step, unlike checked. The table sums up which call answers which question:

You want jQuery DOM
Is it ticked, selected, disabled now? .prop('checked') el.checked
What it holds now .val() el.value
The default from the HTML .attr('value'), .attr('checked') !== undefined el.defaultValue, el.defaultChecked

Checkpoint

Check: Attributes and properties

  1. Question 1 of 2 The newsletter box was ticked in the HTML, and the user has just unticked it. What do .prop('checked') and the checked attribute say now?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: .prop('checked') is false; the attribute is still there

    The checked attribute is the default state, and the checked property is the state now. jQuery's documentation stresses that the attribute does not correspond to the property.

  2. Question 2 of 2 What does $('#calls').val() return for a checkbox that has no value attribute and is not ticked?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: "on"

    .val() of a checkbox is the value it sends when ticked, never whether it is ticked, so code that reads the state from .val() is always wrong.

jQuery 4 returns boolean attributes as the HTML writes them

checked, selected and disabled are boolean attribute: An HTML attribute such as checked, selected or disabled whose presence means true and absence false. Its only valid values are an empty string and its own name; true and false are not allowed.: their presence means true, their absence false, and the HTML Standard allows only an empty string or the attribute’s own name as a value (HTML Standard). From 1.6 to 3.x, .attr() papered over this: it returned the name whatever the HTML said, and stored the name whatever it was given. jQuery 4 returns and stores the real value (.attr()):

booleans.js, on the same form JavaScript · prefs/booleans.js
// booleans.js: .attr() on boolean attributes. jQuery 4 returns and stores what the attribute really holds.
const q = (value) => JSON.stringify(value);
const box = (id) => document.getElementById(id);

console.log(`.attr('checked'): news ${q($('#news').attr('checked'))}, sms ${q($('#sms').attr('checked'))}, calls ${q($('#calls').attr('checked'))}`);
console.log(`if ($('#news').attr('checked')): ${$('#news').attr('checked') ? 'runs' : 'skipped'}`);
console.log(`.attr('disabled') of save: ${q($('#save').attr('disabled'))}`);
console.log(`.attr('selected') of Hindi: ${q($('#lang option[value=hi]').attr('selected'))}`);

$('#calls').attr('checked', 'yes');
console.log(`.attr('checked', 'yes') stores ${q(box('calls').getAttribute('checked'))}`);
$('#calls').attr('checked', true);
console.log(`.attr('checked', true) stores ${q(box('calls').getAttribute('checked'))}`);
$('#calls').attr('checked', false);
console.log(`.attr('checked', false) leaves the attribute: ${box('calls').hasAttribute('checked')}`);
console.log(`.prop('checked') of news, either way: ${$('#news').prop('checked')}`);
What booleans.js logged on 4.0.0, and where 3.7.1 differed JavaScript · booleans.mjs
// Prints what prefs/booleans.js logged in jsdom (recorded.mjs).
import { show } from './show.mjs';

show('prefs/booleans.html');

Output

.attr('checked'): news "", sms "CHECKED", calls undefined
if ($('#news').attr('checked')): skipped
.attr('disabled') of save: ""
.attr('selected') of Hindi: ""
.attr('checked', 'yes') stores "yes"
.attr('checked', true) stores "true"
.attr('checked', false) leaves the attribute: false
.prop('checked') of news, either way: true
(jQuery 3.7.1 logged other lines:)
  line 1: .attr('checked'): news "checked", sms "checked", calls undefined
  line 2: if ($('#news').attr('checked')): runs
  line 3: .attr('disabled') of save: "disabled"
  line 4: .attr('selected') of Hindi: "selected"
  line 5: .attr('checked', 'yes') stores "checked"
  line 6: .attr('checked', true) stores "checked"

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node booleans.mjs

Call jQuery 3.7.1 jQuery 4.0.0
.attr('checked') on <input checked> "checked" ""
.attr('checked') on checked="CHECKED" "checked" "CHECKED"
if ($box.attr('checked')) runs skipped
.attr('checked', 'yes') stores "checked" "yes"
.attr('checked', true) stores "checked" "true"

The empty string is the dangerous line: it is falsy, so every if that tested the getter now takes the other branch. Neither version’s getter ever said whether the box was ticked: the .prop('checked') line of the output is the call that does, on both versions.

Testing a box with the attribute getter

Wrong: if ($('#news').attr('checked')), which is false for <input checked> on jQuery 4 and ignores the user’s clicks on every version. Right: if ($('#news').prop('checked')) for the state, or el.hasAttribute('checked') when you really mean the HTML’s default.

Checkpoint

Check: Boolean attributes in jQuery 4

  1. Question 1 of 2 On jQuery 4.0.0, what does $('#news').attr('checked') return for <input type="checkbox" id="news" checked>?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: An empty string, so if ($("#news").attr("checked")) is false

    jQuery's attr() documentation lists "" for 4.0+ and "checked" for 1.6–3.x. Ask .prop('checked') for the state, or test el.hasAttribute('checked') for the default.

  2. Question 2 of 2 What does $('#calls').attr('checked', 'yes') store on jQuery 4.0.0?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: checked="yes"

    The HTML Standard allows only "" or the attribute's name as a boolean attribute's value, so write .attr('checked', 'checked') when you mean the default, or .prop('checked', true) for the state.

.attr(name, false) now removes the attribute

In jQuery 3, passing false to .attr() removed boolean attributes and stored the text "false" in every other one. In jQuery 4, false removes any attribute whose name does not start with aria-; an ARIA attribute still gets the text "false", because for aria-hidden and its relatives “false” is a value with a meaning (.attr()):

attr-false.js, on the same form JavaScript · prefs/attr-false.js
// attr-false.js: .attr(name, false) on attributes that are not boolean.
const q = (value) => JSON.stringify(value);
const state = (el, name) => (el.hasAttribute(name) ? `kept as ${q(el.getAttribute(name))}` : 'removed');
const help = document.getElementById('help');
const panel = document.getElementById('panel');

$('#help').attr('title', false);
console.log(`.attr('title', false): title ${state(help, 'title')}`);
$('#panel').attr('data-mode', false);
console.log(`.attr('data-mode', false): data-mode ${state(panel, 'data-mode')}`);
$('#panel').attr('aria-hidden', false);
console.log(`.attr('aria-hidden', false): aria-hidden ${state(panel, 'aria-hidden')}`);

$('#help').attr('title', 'false');
console.log(`.attr('title', 'false'): title ${state(help, 'title')}`);
$('#help').attr('title', null);
console.log(`.attr('title', null): title ${state(help, 'title')}`);
$('#panel').removeAttr('data-mode');
console.log(`.removeAttr('data-mode'): data-mode ${state(panel, 'data-mode')}`);
What attr-false.js logged on 4.0.0, and where 3.7.1 differed JavaScript · attr-false.mjs
// Prints what prefs/attr-false.js logged in jsdom (recorded.mjs).
import { show } from './show.mjs';

show('prefs/attr-false.html');

Output

.attr('title', false): title removed
.attr('data-mode', false): data-mode removed
.attr('aria-hidden', false): aria-hidden kept as "false"
.attr('title', 'false'): title kept as "false"
.attr('title', null): title removed
.removeAttr('data-mode'): data-mode removed
(jQuery 3.7.1 logged other lines:)
  line 1: .attr('title', false): title kept as "false"
  line 2: .attr('data-mode', false): data-mode kept as "false"

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node attr-false.mjs

The title and the data-mode attribute went on 4.0.0 and stayed as "false" on 3.7.1. Code that stored the text on purpose, often a data- flag that other code reads back, breaks one way; code that meant to remove the attribute and got lucky with a boolean one works by accident. Say what you mean on both versions: .attr(name, 'false') for the text, as jQuery Migrate advises (warnings.md), and .removeAttr(name) for removal. .attr(name, null) removed the title on both versions as well.

Checkpoint

Check: .attr(name, false) in jQuery 4

  1. Question 1 of 2 What does $('#help').attr('title', false) do on jQuery 4.0.0?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It removes the title attribute

    jQuery 3 removed only boolean attributes with false and stored "false" in the others. Code that relied on the text needs .attr(name, 'false'); code that meant removal is clearer with .removeAttr(name).

  2. Question 2 of 2 And $('#panel').attr('aria-hidden', false) on jQuery 4.0.0?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It stores aria-hidden="false"

    For aria-hidden, aria-expanded and the other ARIA states, "false" is meaningful, so jQuery 4 still writes it.

toggleClass() without a class name changes nothing in jQuery 4

toggleClass() has two families of forms. With a class name it flips that class, or sets it from a boolean state: toggleClass('open', true) adds, toggleClass('open', false) removes. Without a class name, toggleClass(false) and toggleClass() were an old trick that removed every class and later restored them; the form was deprecated in 3.0 and removed in 4.0 (.toggleClass()). The upgrade guide names the real danger: a variable that “turned out to be undefined or false” removed all classes (4.0 upgrade guide). The script runs each form from class="card open":

toggle.js, on the same form JavaScript · prefs/toggle.js
// toggle.js: every form of toggleClass() that legacy code uses, each one started from class="card open".
const $panel = $('#panel');
const tryIt = (label, call) => {
  $panel.attr('class', 'card open');
  call();
  console.log(`${label} -> class ${JSON.stringify($panel.attr('class'))}`);
};
let flag;

tryIt('toggleClass(false)', () => $panel.toggleClass(false));
tryIt('toggleClass(flag), flag undefined', () => $panel.toggleClass(flag));
tryIt("toggleClass('open')", () => $panel.toggleClass('open'));
tryIt("toggleClass('open', false)", () => $panel.toggleClass('open', false));
tryIt("toggleClass('open', true)", () => $panel.toggleClass('open', true));
tryIt("toggleClass('open', flag), flag undefined", () => $panel.toggleClass('open', flag));
tryIt("toggleClass('open', Boolean(flag))", () => $panel.toggleClass('open', Boolean(flag)));
What toggle.js logged on 4.0.0, and where 3.7.1 differed JavaScript · toggle.mjs
// Prints what prefs/toggle.js logged in jsdom (recorded.mjs).
import { show } from './show.mjs';

show('prefs/toggle.html');

Output

toggleClass(false) -> class "card open"
toggleClass(flag), flag undefined -> class "card open"
toggleClass('open') -> class "card"
toggleClass('open', false) -> class "card"
toggleClass('open', true) -> class "card open"
toggleClass('open', flag), flag undefined -> class "card"
toggleClass('open', Boolean(flag)) -> class "card"
(jQuery 3.7.1 logged other lines:)
  line 1: toggleClass(false) -> class ""
  line 2: toggleClass(flag), flag undefined -> class ""

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node toggle.mjs

On 3.7.1 the two forms without a class name wiped the panel’s classes; on 4.0.0 they changed nothing. The other lines were the same on both, and one of them is a bug of its own: toggleClass('open', flag) with an undefined flag toggled, because the state must be “a boolean (not just truthy/falsy)” (.toggleClass()). Boolean(flag) turns it into one and removed open as intended.

Key idea

Give toggleClass() a class name and a real boolean, or use addClass() and removeClass(): the forms without a name are gone, and an undefined state flips the class.

Checkpoint

Check: The forms of toggleClass()

  1. Question 1 of 2 The panel has class="card open" and flag is undefined. What does $panel.toggleClass(flag) do on jQuery 4.0.0?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Nothing; the classes stay card open

    The upgrade guide explains that a variable that turned out undefined or false used to remove all classes. Now such a call changes nothing, which is safer but still a bug in the calling code.

  2. Question 2 of 2 You want the open class on when show is true and off otherwise, and show may be undefined. Which call is right on jQuery 3 and 4?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: $panel.toggleClass('open', Boolean(show))

    jQuery's documentation asks for "a boolean (not just truthy/falsy)" state. Convert it with Boolean(), or use addClass() and removeClass() in an if.

jQuery Migrate finds the changes; native code says what it means

The four changes leave no error to search for, but jQuery Migrate’s development build reports each one, and you can start before the upgrade: Migrate 3.6.0 on jQuery 3.7.1 already warns about them while the calls still behave the old way. The upgrade script below runs the four calls on both pairs:

Interactive · stage by stage Finding the attribute changes before and after the upgrade Step through the upgrade of the settings page and watch where each warning comes from and when it stops.
Finding the attribute changes before and after the upgrade, step 4 of 4. Showing 3.7.1 + Migrate 3.6.0, Four warnings, Fix each call, 4.0.0 + Migrate 4.0.2, Remove Migrate. In focus: Remove Migrate.a new warning: fix it3.7.1 + Migrate 3.6.0jQuery 3, in developmentFour warningsone for each changed callFix each callprop(), removeAttr(), data4.0.0 + Migrate 4.0.2jQuery 4, the same pagesRemove Migrateno warning left

The drawing shows the whole scene; its 4 steps are listed below.

Every step as text
  1. Warnings on jQuery 3. Migrate 3.6.0 on jQuery 3.7.1 already warned about all four calls of the settings page, while they still behaved the jQuery 3 way.
  2. Fix them first. Read state with .prop(), store "false" as a string, remove attributes with removeAttr(), and keep classes in your own data. The page then behaves the same on 3 and on 4.
  3. Upgrade with Migrate 4. On jQuery 4.0.0, Migrate 4.0.2 restores the jQuery 3 behaviour of each call it finds and warns, so a call you missed still works while it tells you where it is.
  4. Remove Migrate. When the pages log no warning, remove Migrate: it is an upgrade tool, and jQuery 4 behaves as the fixed code expects.

upgrade.js, on the same form with Migrate JavaScript · prefs/upgrade.js
// upgrade.js: the four changed behaviours of this lesson, run with jQuery Migrate on the page.
const q = (value) => JSON.stringify(value);
const box = (id) => document.getElementById(id);

console.log(`.attr('checked') of news: ${q($('#news').attr('checked'))}`);
$('#calls').attr('checked', 'yes');
console.log(`.attr('checked', 'yes') stores ${q(box('calls').getAttribute('checked'))}`);
$('#help').attr('title', false);
console.log(`.attr('title', false): the title is ${box('help').hasAttribute('title') ? 'kept' : 'removed'}`);
let flag;
$('#panel').toggleClass(flag);
console.log(`toggleClass(flag), flag undefined -> class ${q($('#panel').attr('class'))}`);
What upgrade.js logged with jQuery Migrate JavaScript · upgrade.mjs
// Prints what prefs/upgrade.js logged in jsdom: first on jQuery 3.7.1 with jQuery Migrate 3.6.0, then on jQuery 4.0.0
// with jQuery Migrate 4.0.2 (recorded.mjs).
import { show } from './show.mjs';

console.log('jQuery 3.7.1 with jQuery Migrate 3.6.0:');
show('prefs/migrate3.html');
console.log('jQuery 4.0.0 with jQuery Migrate 4.0.2:');
show('prefs/migrate4.html');

Output

jQuery 3.7.1 with jQuery Migrate 3.6.0:
JQMIGRATE: Migrate is installed with logging active, version 3.6.0
(warning) JQMIGRATE: Boolean attribute 'checked' value is different from its lowercased name
.attr('checked') of news: "checked"
(warning) JQMIGRATE: Boolean attribute 'checked' is not set to its lowercased name
.attr('checked', 'yes') stores "checked"
(warning) JQMIGRATE: Setting the non-ARIA non-boolean attribute 'title' to false
.attr('title', false): the title is kept
(warning) JQMIGRATE: jQuery.fn.toggleClass( boolean ) is deprecated
toggleClass(flag), flag undefined -> class ""
jQuery 4.0.0 with jQuery Migrate 4.0.2:
JQMIGRATE: Migrate is installed with logging active, version 4.0.2
(warning) JQMIGRATE: Boolean attribute 'checked' value is different from its lowercased name
.attr('checked') of news: "checked"
(warning) JQMIGRATE: Boolean attribute 'checked' is not set to its lowercased name
.attr('checked', 'yes') stores "checked"
(warning) JQMIGRATE: Setting the non-ARIA non-boolean attribute 'title' to false
.attr('title', false): the title is kept
(warning) JQMIGRATE: jQuery.fn.toggleClass( [ boolean ] ) is removed
toggleClass(flag), flag undefined -> class ""

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node upgrade.mjs

On jQuery 4.0.0, Migrate 4.0.2 also brought the jQuery 3 behaviour back for each call, so a missed line keeps working while its warning names it: boolean-attributes, attr-false and toggleClass-bool in Migrate’s list (warnings.md). That makes it an upgrade tool, not a place to stay: once no page logs a warning, remove it.

Code that leaves jQuery behind can say what it means directly, with properties, classList and toggleAttribute() (DOM Standard):

native.js, on the same form without jQuery JavaScript · prefs/native.js
// native.js: the same work without jQuery, each line saying what it means.
const panel = document.getElementById('panel');
const news = document.getElementById('news');
const help = document.getElementById('help');
const q = (value) => JSON.stringify(value);

console.log(`news.checked: ${news.checked}; news.hasAttribute('checked'): ${news.hasAttribute('checked')}`);
news.checked = false;
console.log(`after news.checked = false: ${news.checked}, the attribute still there: ${news.hasAttribute('checked')}`);

let flag;
panel.classList.toggle('open', Boolean(flag));
console.log(`classList.toggle('open', Boolean(flag)): ${q(panel.className)}`);
panel.classList.add('open');
console.log(`classList.add('open'): ${q(panel.className)}, contains: ${panel.classList.contains('open')}`);

panel.toggleAttribute('hidden', true);
console.log(`toggleAttribute('hidden', true): ${q(panel.getAttribute('hidden'))}`);
panel.toggleAttribute('hidden', false);
console.log(`toggleAttribute('hidden', false): hidden ${panel.hasAttribute('hidden') ? 'kept' : 'removed'}`);

help.removeAttribute('title');
panel.setAttribute('aria-hidden', 'false');
console.log(`removeAttribute('title'): ${help.hasAttribute('title') ? 'kept' : 'removed'}; aria-hidden: ${q(panel.getAttribute('aria-hidden'))}`);
What native.js logged JavaScript · native.mjs
// Prints what prefs/native.js logged in jsdom (recorded.mjs).
import { show } from './show.mjs';

show('prefs/native.html');

Output

news.checked: true; news.hasAttribute('checked'): true
after news.checked = false: false, the attribute still there: true
classList.toggle('open', Boolean(flag)): "card"
classList.add('open'): "card open", contains: true
toggleAttribute('hidden', true): ""
toggleAttribute('hidden', false): hidden removed
removeAttribute('title'): removed; aria-hidden: "false"

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node native.mjs

Each jQuery call of this lesson has a native form that says the same thing:

jQuery Native
.prop('checked'), .prop('checked', true) el.checked, el.checked = true
.addClass(c), .removeClass(c), .hasClass(c) el.classList.add(c), remove(c), contains(c)
.toggleClass(c, Boolean(on)) el.classList.toggle(c, Boolean(on))
.attr('hidden', flag) on jQuery 3 el.toggleAttribute('hidden', flag)
.removeAttr(name) el.removeAttribute(name)
.attr('aria-hidden', 'false') el.setAttribute('aria-hidden', 'false')

Checkpoint

Check: Migrate warnings and native code

  1. Question 1 of 2 When can jQuery Migrate first warn about these four attribute changes?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: While the site still runs jQuery 3.7.1, with Migrate 3.6.0

    Fixing the warnings before the upgrade leaves less to change on the day jQuery 4 arrives. With Migrate 4.0.2 on jQuery 4, a missed call keeps its old behaviour and warns, which makes the upgrade safe to roll out.

  2. Question 2 of 2 Which native line says the same as $panel.toggleClass('open', Boolean(show))?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: panel.classList.toggle('open', Boolean(show))

    classList.add(), remove(), contains() and toggle(name, force) cover jQuery's class methods one to one.

Practise: helpers that mean the same on jQuery 3 and 4

Five small helpers of an old settings page rely on the behaviour that changed. Rewrite them so each does what its comment says on both versions; the sample tests run every helper on a stand-in of each and run in your browser:

Exercise · Medium · JavaScript

Fix attribute code so jQuery 3 and 4 agree

The settings page of a shop has five small helpers in settings.mjs, written years ago for jQuery 1.x and 3.x. Each one relies on behaviour that jQuery 4 changed, and some were never right. Rewrite them so that every helper does what its comment says on jQuery 3 and on jQuery 4, which the tests check by running each helper on both.

  • isTicked($box) returns true when the checkbox is ticked now, as the user sees it.
  • tick($box) ticks the checkbox, even after the user has unticked it.
  • clearTitle($el) removes the element's title attribute.
  • saveClasses($el) keeps the element's classes in the element's jQuery data under the key savedClass and removes the class attribute; restoreClasses($el) puts the saved classes back.

The sample tests run without a browser, so they pass two small stand-ins for jQuery, one that behaves like jQuery 3.7 and one like jQuery 4.0 in exactly the calls this lesson covers: .attr(), .removeAttr(), .prop(), .toggleClass(), .addClass(), .removeClass() and .data(). A checkbox in the stand-ins keeps its attribute and its live state apart, as a browser does.

Starter code · settings.mjs

/** True when the checkbox is ticked now, as the user sees it. @param {object} $box */
export function isTicked($box) {
  return Boolean($box.attr('checked'));
}

/** Ticks the checkbox, even after the user has unticked it. @param {object} $box */
export function tick($box) {
  $box.attr('checked', true);
}

/** Removes the element's title attribute. @param {object} $el */
export function clearTitle($el) {
  $el.attr('title', false);
}

/** Keeps the element's classes in its jQuery data (savedClass) and removes the class attribute. @param {object} $el */
export function saveClasses($el) {
  $el.toggleClass(false);
}

/** Puts the classes that saveClasses() kept back on the element. @param {object} $el */
export function restoreClasses($el) {
  $el.toggleClass();
}
The sample tests · settings.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { isTicked, tick, clearTitle, saveClasses, restoreClasses } from './settings.mjs';

// Two stand-ins for jQuery, for one element at a time, in the calls this lesson covers. An element keeps its
// attributes apart from its live checked state, as a browser does: a click or .prop() sets the state, and from then on
// the attribute no longer decides it. The v3 stand-in behaves as jQuery 3.7 does, the v4 one as jQuery 4.0.
const BOOLEAN = new Set(['checked', 'selected', 'disabled', 'readonly', 'required', 'multiple', 'hidden', 'open']);
function element(attrs) {
  return {
    attrs: new Map(Object.entries(attrs)),
    dirty: false,
    state: false,
    get checked() {
      return this.dirty ? this.state : this.attrs.has('checked');
    },
    set checked(value) {
      this.dirty = true;
      this.state = Boolean(value);
    },
  };
}
const click = (el) => {
  el.checked = !el.checked;
};
function standIn(version) {
  return (el) => {
    const data = (el.data ??= new Map());
    const classes = () => (el.attrs.get('class') ?? '').split(/\s+/).filter(Boolean);
    const $el = {
      attr(name, value) {
        if (value === undefined) {
          if (!el.attrs.has(name)) return undefined;
          return version === 3 && BOOLEAN.has(name) ? name : el.attrs.get(name);
        }
        const removes = value === null || (value === false && (version === 4 ? !name.startsWith('aria-') : BOOLEAN.has(name)));
        if (removes) el.attrs.delete(name);
        else el.attrs.set(name, version === 3 && BOOLEAN.has(name) ? name : String(value));
        return $el;
      },
      removeAttr(name) {
        el.attrs.delete(name);
        return $el;
      },
      prop(name, value) {
        if (name !== 'checked') throw new Error('the stand-in knows only the checked property');
        if (value === undefined) return el.checked;
        el.checked = value;
        return $el;
      },
      addClass(name) {
        el.attrs.set('class', [...new Set([...classes(), name])].join(' '));
        return $el;
      },
      removeClass(name) {
        el.attrs.set('class', classes().filter((c) => c !== name).join(' '));
        return $el;
      },
      toggleClass(name, state) {
        if (typeof name === 'string') {
          const on = state === undefined ? !classes().includes(name) : state;
          return on ? $el.addClass(name) : $el.removeClass(name);
        }
        // toggleClass(), toggleClass(false) and toggleClass(undefined): jQuery 3 stashed and restored every class;
        // jQuery 4 removed these forms, so they change nothing.
        if (version === 3) {
          if (classes().length) {
            data.set('__className__', el.attrs.get('class'));
            el.attrs.set('class', '');
          } else if (name !== false) {
            el.attrs.set('class', data.get('__className__') ?? '');
          }
        }
        return $el;
      },
      data(key, value) {
        if (value === undefined) return data.get(key);
        data.set(key, value);
        return $el;
      },
    };
    return $el;
  };
}
const VERSIONS = [3, 4];

test('isTicked() reports what the user sees, on jQuery 3 and 4', () => {
  for (const v of VERSIONS) {
    const $ = standIn(v);
    const news = element({ type: 'checkbox', checked: '' });
    assert.equal(isTicked($(news)), true, `jQuery ${v}: a box ticked in the HTML`);
    click(news);
    assert.equal(isTicked($(news)), false, `jQuery ${v}: the same box after the user unticked it`);
    const calls = element({ type: 'checkbox' });
    click(calls);
    assert.equal(isTicked($(calls)), true, `jQuery ${v}: a box the user ticked`);
  }
});

test('tick() ticks a box, even one the user unticked, on jQuery 3 and 4', () => {
  for (const v of VERSIONS) {
    const $ = standIn(v);
    const sms = element({ type: 'checkbox', checked: 'checked' });
    click(sms);
    tick($(sms));
    assert.equal(sms.checked, true, `jQuery ${v}: the box the user had unticked`);
    const calls = element({ type: 'checkbox' });
    tick($(calls));
    assert.equal(calls.checked, true, `jQuery ${v}: a box that was never ticked`);
  }
});

test('clearTitle() removes the title attribute on jQuery 3 and 4', () => {
  for (const v of VERSIONS) {
    const $ = standIn(v);
    const help = element({ href: '/help/', title: 'Help with settings' });
    clearTitle($(help));
    assert.equal(help.attrs.has('title'), false, `jQuery ${v}: the title attribute`);
    assert.equal(help.attrs.get('href'), '/help/', `jQuery ${v}: the other attributes stay`);
  }
});

test('saveClasses() and restoreClasses() keep and bring back the classes on jQuery 3 and 4', () => {
  for (const v of VERSIONS) {
    const $ = standIn(v);
    const panel = element({ class: 'card open' });
    saveClasses($(panel));
    assert.equal((panel.attrs.get('class') ?? '').trim(), '', `jQuery ${v}: no class while saved`);
    assert.equal($(panel).data('savedClass'), 'card open', `jQuery ${v}: the saved classes`);
    restoreClasses($(panel));
    assert.equal(panel.attrs.get('class'), 'card open', `jQuery ${v}: the classes restored`);
  }
});
A hint

Ask what each helper means, then pick the call that says exactly that on both versions: the live state of a box is a property (.prop('checked'), .prop('checked', true)), removing an attribute is .removeAttr(), and saving classes is .data('savedClass', $el.attr('class')) followed by .removeAttr('class'), with .attr('class', …) to restore them. None of these calls changed in jQuery 4.

The sample tests run on this device, in your browser (QuickJS): nothing is sent to mysmartcopilot.com. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

HTML, CSS & JS Playground (Online Editor) Load jQuery 3.7.1 and then 4.0.0 in the playground and run the settings form's scripts against your own markup.

Then the questions of the lesson, one at a time; the ones on the ideas you have answered best come last, and every choice says what it gets right or misses:

Practice that adapts to you

Practise this lesson

12 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 12 Which call tells you whether a checkbox is ticked now?

    Choose one answer.

    Show the answer: Question 1 of 12

    Answer: .prop('checked')

    jQuery's documentation calls if ($(elem).prop('checked')) the reliable test. The same goes for selected and disabled.

  2. Question 2 of 12 The PIN field was written as value="411001", and the user typed 560001. What does .attr('value') return?

    Choose one answer.

    Show the answer: Question 2 of 12

    Answer: "411001", the default from the HTML

    value is a dynamic attribute like checked and selected: the attribute is the default (also defaultValue), the property the current state.

  3. Question 3 of 12 Why does it matter that the attributes keep the defaults from the HTML?

    Choose one answer.

    Show the answer: Question 3 of 12

    Answer: Resetting the form restores every field from them

    Changing an attribute changes the default that a reset brings back. To change what the user sees now, change the property.

  4. Question 4 of 12 Which values does the HTML Standard allow for a boolean attribute such as checked?

    Choose one answer.

    Show the answer: Question 4 of 12

    Answer: An empty string or the attribute's own name, in any case

    jQuery 4 no longer rewrites what you pass, so your code is responsible for a valid value: .attr('checked', 'checked'), or better .prop() for the state.

  5. Question 5 of 12 What does $('#calls').attr('checked', true) store on jQuery 4.0.0?

    Choose one answer.

    Show the answer: Question 5 of 12

    Answer: checked="true"

    jQuery Migrate reports this case as "Boolean attribute 'checked' is not set to its lowercased name". Set the default with 'checked', and the state with .prop('checked', true).

  6. Question 6 of 12 Before an upgrade, you search for code that reads boolean attributes with .attr(). Which line is fine to keep on jQuery 4?

    Choose one answer.

    Show the answer: Question 6 of 12

    Answer: if ($box.attr('checked') !== undefined)

    Presence is the meaning of a boolean attribute. Test it with !== undefined or el.hasAttribute(), and use .prop() when you mean the current state.

  7. Question 7 of 12 A widget stores data-open="false" with .attr('data-open', false) and reads it back later. How do you keep that working on jQuery 4?

    Choose one answer.

    Show the answer: Question 7 of 12

    Answer: Write .attr('data-open', 'false'), with the value as a string

    jQuery Migrate's attr-false warning gives the same fix: wrap the value in quotes when you want the text false.

  8. Question 8 of 12 Which call removes an attribute on every jQuery version?

    Choose one answer.

    Show the answer: Question 8 of 12

    Answer: .removeAttr(name)

    .attr(name, null) also removes on both versions, but removeAttr() says what it means.

  9. Question 9 of 12 Legacy code hides a panel's styling with $p.toggleClass(false) and brings it back with $p.toggleClass(). What replaces it on jQuery 4?

    Choose one answer.

    Show the answer: Question 9 of 12

    Answer: Save $p.attr('class') in the element's data, remove the attribute, and set it back from the data

    Being explicit about which classes to toggle is the upgrade guide's advice; when the old trick is really needed, keep the class string yourself.

  10. Question 10 of 12 What does $panel.toggleClass('open') do to class="card open"?

    Choose one answer.

    Show the answer: Question 10 of 12

    Answer: It removes open, leaving card

    toggleClass(className) and toggleClass(className, state) are unchanged in jQuery 4; the removed forms are the ones without a class name.

  11. Question 11 of 12 Which native call adds the hidden attribute when show is false and removes it when show is true?

    Choose one answer.

    Show the answer: Question 11 of 12

    Answer: el.toggleAttribute('hidden', !show)

    For a boolean attribute, toggleAttribute(name, force) is the native counterpart of jQuery's .attr(name, flag) of jQuery 3, without its ambiguity.

  12. Question 12 of 12 The team wants to keep jQuery Migrate 4.0.2 in production permanently because it makes the old attribute code work. What is the problem?

    Choose one answer.

    Show the answer: Question 12 of 12

    Answer: Migrate is an upgrade tool; it keeps behaviour jQuery 4 removed, so the code never gets fixed

    Run Migrate's development build until the pages log no warning, then remove it. Each warning is a line of code that would behave differently without it.

Interview questions

Warm-up (fresher to 2–5 years): when do you use .prop() rather than .attr()?

Use .prop() for the live state of an element: checked, selected, disabled and the current value, which change as the user clicks and types. Use .attr() for what the HTML says, such as href, title, data- and aria- attributes, or a field’s default. For a checkbox the two differ as soon as the user clicks it: the checked attribute keeps the default from the HTML, which form.reset() brings back, while the checked property follows the click. .attr('checked') was never a test of the state, and on jQuery 4 it even returns "" for <input checked>, which is falsy.

The bank of this module has two deeper questions about upgrades: a “remember me” box that stopped working, and an audit of the silent attribute changes before jQuery 4 goes live. Draft an answer to each before you open its first step:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Fresher to senior

    A login form's "Remember me" box is ticked in the HTML. Since the upgrade to jQuery 4, nobody is remembered; the code decides with if ($('#remember').attr('checked')). Explain what changed, why the code was already wrong on jQuery 3, and fix it.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    Before a large site moves from jQuery 3.7 to 4.0, you are asked to find every place where .attr() and toggleClass() will change behaviour without an error. How do you plan the audit, and how do you know you are done?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • Attributes are the HTML’s defaults and properties the live state: read and set checked, selected, disabled and the current value with .prop() or .val(), and remember that a checkbox’s .val() is its value, not its state.
  • jQuery 4 returns boolean attributes as written, "" for <input checked>, and stores what it is given, so if ($box.attr('checked')) and .attr('checked', true) now mean something else.
  • .attr(name, false) removes every attribute but aria-* ones in jQuery 4; write .attr(name, 'false') for the text and .removeAttr(name) for removal.
  • toggleClass() without a class name changes nothing in jQuery 4, and an undefined state flips the class, so pass a class name and Boolean(state).
  • jQuery Migrate 3.6.0 warns about these changes on jQuery 3.7.1 and Migrate 4.0.2 on 4.0.0, and native code says the same with properties, classList and toggleAttribute().

Terms in this lesson

boolean attribute
An HTML attribute such as checked, selected or disabled whose presence means true and absence false. Its only valid values are an empty string and its own name; true and false are not allowed.
DOM property
A value on the JavaScript object of an element, such as el.checked or el.value. The browser sets it from the HTML attribute when the page loads; after that, clicks and typing change the property and leave the attribute as the default.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.