jQuery to JavaScript Converter
Translates jQuery a statement at a time, so a setter becomes a loop and a getter becomes one element, and leaves what it cannot translate exactly as it was.
Lines in 17
Statements translated 8
Left alone, needs a decision 2
Left alone, no rule fits 0
What was translated
ready ×1 DOMContentLoaded fires once, so a script that loads after it will not run the callback. jQuery ran it anyway; for a deferred or dynamically inserted script, check document.readyState first.
hide, write ×1 jQuery stored the previous display value so show() could restore it. Nothing native does, which is why show() below sets an empty string and falls back to the stylesheet.
each ×1 jQuery passed (index, element) and bound this to the element; forEach passes (element, index) and binds nothing. Any `this` in the body becomes the element parameter, and the closing `});` needs checking.
attr, write ×1 Direct equivalent.
on ×1 A for..of loop rather than forEach, so a `return` inside the handler still returns from the handler. The element is in scope as `element`, which is what `this` used to be, and the statement now needs two closing braces.
addClass, write ×1 classList.add takes one class per argument, where jQuery took a space-separated string.
val, read ×1 Reading a multiple select gave an array in jQuery and gives one string here; use [ ...el.selectedOptions ].map( o => o.value ) for that case.
html, write ×1 innerHTML with untrusted content is an XSS. textContent unless the markup is yours.
What needs a decision
line 15 jQuery animations have no native equivalent. Use a CSS transition, or element.animate() from the Web Animations API, both of which run on the compositor and are smoother than jQuery ever was.
line 16 This is a plugin rather than jQuery. Replace it or keep jQuery for this page: converting the code around a plugin and keeping the plugin means shipping the library anyway.
JavaScript
// Two helpers, which is most of what jQuery was for.
const $$ = ( selector, root = document ) => [ ...root.querySelectorAll( selector ) ];
const $1 = ( selector, root = document ) => root.querySelector( selector );
document.addEventListener( 'DOMContentLoaded', () => {
$$( ".alert" ).forEach( ( element ) => element.style.display = 'none' );
$$( ".tabs a" ).forEach( ( el, i ) => {
el.setAttribute( "data-index", i );
});
for ( const element of $$( ".btn" ) ) {
element.addEventListener( 'click', ( e ) => {
$$( ".panel" ).forEach( ( element ) => element.classList.add( "is-open" ) );
});
var label = $1( "#name" )?.value;
$$( "#out" ).forEach( ( element ) => element.innerHTML = "<b>" + label + "</b>" );
// TODO, jQuery: jQuery animations have no native equivalent. Use a CSS transition, or element.animate() from the Web Animations API, both of which run on the compositor and are smoother than jQuery ever was.
$(".legacy").fadeIn(200);
// TODO, jQuery: This is a plugin rather than jQuery. Replace it or keep jQuery for this page: converting the code around a plugin and keeping the plugin means shipping the library anyway.
$("#picker").datepicker({ format: "yyyy-mm-dd" });
});
This is a translation guide rather than a compiler. It matches
statements with a pattern and does not parse the JavaScript, so it
cannot tell code from a string, and a line it does not recognise is left
exactly as it was rather than guessed at. Read the output as a draft and
run the tests.
Setters become a loop and getters become one element, because that is
the difference jQuery hid: `$(".item").addClass("on")` touched every
match, and `$(".item").text()` read the first one. A converter that
translates fragments cannot make that distinction and produces code that
looks right and does nothing.
2 lines have no equivalent at all: an animation, a plugin, a Deferred or
a geometry method. Those are listed above with what to use instead, and
they are the work a converter cannot do for you.
Three differences cause most of the bugs in a migration. A
jQuery-triggered event is invisible to a native listener and the
reverse, so a half-migrated page breaks in the middle. `.off()` needs
the same function reference that was passed to `.on()`, so an inline
arrow function can never be removed. And `this` inside a jQuery callback
was the element, which an arrow function does not preserve.
Check whether you need to convert at all. jQuery is about 30 KB gzipped
and it is not slow; the reason to remove it is to drop a dependency, and
a page that keeps one plugin keeps the whole library. Removing it from a
theme that also loads it for the admin bar saves nothing.
What replaces it is mostly the platform: querySelectorAll, classList,
closest, matches, dataset, Element.append, fetch and element.animate are
in every browser in use. The gap is terseness, and the two helpers at
the top of the output close most of it.
Output is valid and updates as you type.
Fix the highlighted fields to update the output.
jQuery’s defining feature is implicit iteration. $('.item').addClass('on') touches every
match; $('.item').text() reads the first one. That single asymmetry is why a converter
that works on fragments cannot be correct: it produces
document.querySelectorAll('#count').textContent = n, which looks right and does nothing,
because a NodeList has no textContent.
So this matches whole statements. A setter becomes a loop, a getter becomes one element, a target that is already an element is not wrapped in a lookup, and anything with no equivalent is left exactly as it was and listed with what to use instead.
It is a translation guide, not a compiler. Read the output as a draft and run your tests.
How to use
- Paste one function or one file.
- Read the summary: what was translated, what needs a decision, and what no rule fits.
- Read the behaviour notes. That is where the migration bugs live.
Example
// Two helpers, which is most of what jQuery was for.
const $$ = ( selector, root = document ) => [ ...root.querySelectorAll( selector ) ];
const $1 = ( selector, root = document ) => root.querySelector( selector );
document.addEventListener( 'DOMContentLoaded', () => {
$$( ".alert" ).forEach( ( element ) => element.style.display = 'none' );
$$( ".tabs a" ).forEach( ( el, i ) => {
el.setAttribute( "data-index", i );
});
for ( const element of $$( ".btn" ) ) {
element.addEventListener( 'click', ( e ) => {
$$( ".panel" ).forEach( ( element ) => element.classList.add( "is-open" ) );
});
var label = $1( "#name" )?.value;
$$( "#out" ).forEach( ( element ) => element.innerHTML = "<b>" + label + "</b>" );
// TODO, jQuery: jQuery animations have no native equivalent…
$(".legacy").fadeIn(200);
// TODO, jQuery: This is a plugin rather than jQuery…
$("#picker").datepicker({ format: "yyyy-mm-dd" });
});
Three things to notice. .hide() and .addClass() became loops because they write.
.val() became $1(…)?.value because it reads the first match. And $(el).attr(…) became
el.setAttribute(…) rather than a lookup, because el was already an element.
The two TODO lines are untouched. An animation and a plugin have no mechanical
translation, and pretending otherwise would be the worst thing this tool could do.
Pitfalls
Closing braces need checking. $(sel).on('click', function(){ becomes two lines, a loop
and a listener, so the statement that closed with }); now needs one more brace. A
line-based converter cannot fix that without parsing, and the output will not run until you
do.
this is not preserved by an arrow function. Inside a jQuery callback this was the
element. In the translated for…of loop the element is in scope as element, and any
remaining this in the body has to be changed to it.
.off() needs the same function reference. element.removeEventListener('click', fn)
only works with the exact fn that was added, so an inline arrow function can never be
removed. If the code uses .off(), the handlers have to become named functions.
jQuery events and native events do not see each other. An event triggered with
.trigger() is invisible to addEventListener, and one dispatched with dispatchEvent is
invisible to a jQuery .on() handler. That is why a half-migrated page breaks in the middle
rather than at the edges: convert a whole feature at a time.
text() and html() are read-or-write depending on the argument. So are val(),
attr() and css(). The converter uses the argument count to decide, which is right for
ordinary code and wrong for anything clever.
innerHTML with untrusted content is an XSS. jQuery’s .html() had the same problem and
people were used to it. If the value comes from a user, use textContent.
A plugin keeps the library. Converting the code around a datepicker and keeping the datepicker means shipping jQuery anyway. Either replace the plugin or accept the dependency.
It does not parse strings or comments. A line inside a template literal that looks like a jQuery call will be translated. Read the diff.
Compatibility
Everything runs in the browser: nothing is uploaded and nothing is stored.
Everything the output uses is in every browser in use: querySelectorAll, classList,
closest, matches, dataset, Element.append, fetch, and element.animate for the
animations this refuses to translate. The two helpers at the top are three lines and close
most of the terseness gap.
The getJSON translation uses await, which means the enclosing function has to be async.
That is deliberate: the alternative is a .then() chain that hides the status check, and the
status check is the thing $.getJSON was quietly skipping.
WordPress bundles jQuery and loads it for the admin bar on the front end of many themes, so
removing it from your own code may not remove it from the page. Check with
wp_scripts()->queue before counting the saving.
Frequently asked questions
Should I remove jQuery at all?
What replaces $.ajax?
fetch, with two differences that matter: it takes the URL first and the options second, and
it does not reject on a 404 or a 500, so response.ok has to be checked. The tool refuses to
translate $.ajax mechanically for exactly that reason.What about $(document).on('click', '.item', handler)?
event.target.closest('.item') at the top of the handler. It is three lines and it behaves
the same, including for elements added later.Do I need the $$ helper?
[ ...document.querySelectorAll(sel) ] inline is
the same thing; the helper exists because that appears on every line otherwise.How do I do animations?
element.animate() for anything sequenced. Both run
on the compositor, which jQuery never did, so they are smoother as well as smaller.