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.

Enable JavaScript to customise; default output below.

One function or one file at a time. The default has two lines in it that cannot be translated, on purpose.

Live preview converted.js
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.

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

  1. Paste one function or one file.
  2. Read the summary: what was translated, what needs a decision, and what no rule fits.
  3. 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?
Sometimes. It is about 30 KB gzipped and it is not slow. The good reasons are dropping a dependency, removing a build step, or moving to a framework that has its own idioms. Raw performance is rarely one of them.
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)?
Delegation has no direct equivalent. Put the listener on the container and check 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?
No, and the output is longer without it. [ ...document.querySelectorAll(sel) ] inline is the same thing; the helper exists because that appears on every line otherwise.
How do I do animations?
A CSS transition for a state change, and element.animate() for anything sequenced. Both run on the compositor, which jQuery never did, so they are smoother as well as smaller.
Weekly drops

New tools, when there are new tools

One email when something worth using ships. No schedule to fill, so no filler.

Your address goes nowhere else, and one click unsubscribes.