jQuery in WordPress & Bootstrap 4
Two of the most common places you will meet jQuery at work are WordPress sites (company websites, blogs, WooCommerce shops) and Bootstrap 4 sites (admin panels and templates bought between 2018 and 2022). Both have their own rules for jQuery, and breaking them causes the classic errors: $ is not a function, plugins that stop working, and modals that never open.
This lesson shows the right way to add jQuery code to a WordPress theme or plugin, how to call the server through admin-ajax.php safely, how Bootstrap 4's jQuery plugins work, and what changes when such a site moves to Bootstrap 5.
How WordPress loads jQuery
WordPress ships its own copy of jQuery and registers it under the script handle (name) jquery. When we checked the WordPress source code in November 2026, WordPress 7.1 still shipped jQuery 3.7.1 together with jQuery Migrate. The upgrade to jQuery 4 is being discussed by the core team (Trac ticket 60478) but has not shipped yet. Two consequences:
- Do not load your own jQuery from a CDN, and do not deregister the core copy to swap in another version. Many plugins and the admin screens are tested against the bundled one, and two copies on a page break plugins (see Debugging jQuery in DevTools).
$is not available. WordPress's copy of jQuery ends withjQuery.noConflict();, which gives$back to whatever owned it before (usually nothing). Code copied from a tutorial that starts with$(...)therefore throws$ is not a function.
Adding a script to a theme, step by step
- Put your JavaScript in its own file, for example
js/enquiry.jsinside your (child) theme. Never paste scripts into theme templates. - Enqueue it from
functions.phpwithjqueryas a dependency, so WordPress prints jQuery first and only once. - Pass server values (the AJAX URL, a security token) from PHP to JavaScript with
wp_localize_script()orwp_add_inline_script(). - Wrap your code in
jQuery(function ($) { ... }), which gives you$safely inside. - Handle the AJAX request in PHP, checking the token with
check_ajax_referer()and answering withwp_send_json_success().
Steps 2, 3 and 5 are PHP in the theme's functions.php:
<?php
// Step 2: enqueue, with jQuery as a dependency, in the footer, deferred
add_action('wp_enqueue_scripts', function () {
wp_enqueue_script(
'ec-enquiry',
get_stylesheet_directory_uri() . '/js/enquiry.js',
['jquery'],
'1.0.0',
['in_footer' => true, 'strategy' => 'defer']
);
// Step 3: creates a global object "enquiryData" before the script runs
wp_localize_script('ec-enquiry', 'enquiryData', [
'ajaxUrl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('ec_enquiry'),
]);
});
// Step 5: the "action" sent by JavaScript decides which hook runs
add_action('wp_ajax_ec_enquiry', 'ec_handle_enquiry'); // logged-in users
add_action('wp_ajax_nopriv_ec_enquiry', 'ec_handle_enquiry'); // visitors
function ec_handle_enquiry(): void {
check_ajax_referer('ec_enquiry', 'nonce'); // stops forged requests
$name = sanitize_text_field(wp_unslash($_POST['name'] ?? ''));
if ($name === '') {
wp_send_json_error(['message' => 'Please enter your name.'], 400);
}
// ... save the enquiry ...
wp_send_json_success(['message' => "Thank you, $name. We will call you back."]);
}A nonce here is a WordPress security token tied to the user and the action. It proves the request came from your own page, which protects against cross-site request forgery. The array form of the fifth argument (in_footer, strategy) was added in WordPress 6.3.
Step 4 is the JavaScript file. The runnable example below imitates a WordPress page: it releases $ with noConflict() exactly as WordPress does, and defines enquiryData the way wp_localize_script() would. A test server stands in for admin-ajax.php:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>jQuery the WordPress way</title>
</head>
<body>
<form id="enquiry">
<label for="name">Your name</label>
<input id="name" name="name" value="Farhan">
<button>Request a call back</button>
</form>
<p id="reply" role="status"></p>
<pre id="check"></pre>
<script src="https://cdn.jsdelivr.net/npm/jquery@4.0.0/dist/jquery.min.js"></script>
<script>
jQuery.noConflict(); // what WordPress's copy of jQuery does
// What wp_localize_script() prints (the test server replaces admin-ajax.php here)
var enquiryData = { ajaxUrl: "https://jsonplaceholder.typicode.com/posts", nonce: "a1b2c3d4e5" };
</script>
<script>
// js/enquiry.js
document.querySelector("#check").textContent = "Outside the wrapper, typeof $ is: " + typeof $;
jQuery(function ($) { // inside, $ is jQuery again
$("#enquiry").on("submit", function (event) {
event.preventDefault();
const $button = $(this).find("button").prop("disabled", true);
$.post(enquiryData.ajaxUrl, {
action: "ec_enquiry", // picks the wp_ajax_ec_enquiry hook in PHP
nonce: enquiryData.nonce,
name: $("#name").val(),
})
.done(function () {
$("#reply").text("Thank you, " + $("#name").val() + ". We will call you back.");
})
.fail(function () {
$("#reply").text("Sorry, something went wrong. Please call us instead.");
})
.always(function () {
$button.prop("disabled", false);
});
});
});
</script>
</body>
</html>On a real site, wp_send_json_success() returns { "success": true, "data": { "message": "..." } }, so you would show response.data.message with .text(). For new features, WordPress's REST API with fetch() is a good modern alternative to admin-ajax.php.
Bootstrap 4 and jQuery
Bootstrap 4's JavaScript components (modal, dropdown, collapse, tooltip, tab and others) are jQuery plugins. Version 4.6.2 is the last release, and the Bootstrap team ended support for version 4 on 1 January 2023. A Bootstrap 4 page needs, in this order:
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css">
...
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"></script>
<!-- bootstrap.bundle includes Popper, which dropdowns and tooltips need -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.bundle.min.js"></script>
<script>
$("#feeModal").modal("show"); // call a method
$("#feeModal").on("shown.bs.modal", function () { // namespaced events
$(this).find("input").first().trigger("focus");
});
$('[data-toggle="tooltip"]').tooltip(); // tooltips are opt-in
</script>You will see three patterns in Bootstrap 4 code: method calls with strings (.modal("hide")), events named show.bs.modal / shown.bs.modal (the "before" one can be cancelled), and HTML-only setup with data-toggle and data-target attributes. These are the same plugin patterns you studied in Advanced Plugin Patterns.
Bootstrap 4's package lists jQuery 1.9.1 to 3 as supported, so jQuery 4 is outside its tested range. Keep Bootstrap 4 pages on jQuery 3.7.1 (which has the security fixes) and plan the move to Bootstrap 5, rather than combining an unsupported pair.
Moving to Bootstrap 5
Bootstrap 5 does not need jQuery. If jQuery is on the page, Bootstrap 5 still adds its components to jQuery's plugin system, so $("#m").modal("show") keeps working while you migrate. The main JavaScript changes:
| Bootstrap 4 | Bootstrap 5 |
|---|---|
data-toggle="modal", data-target="#m" | data-bs-toggle="modal", data-bs-target="#m" |
$("#m").modal("show") | bootstrap.Modal.getOrCreateInstance("#m").show() (the jQuery form still works if jQuery is present) |
| Needs jQuery and Popper 1 | No jQuery; Popper 2 (included in bootstrap.bundle) |
$(el).on("shown.bs.modal", fn) | el.addEventListener("shown.bs.modal", fn), or .on() with jQuery |
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Bootstrap 5 with and without jQuery</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="p-3">
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#fee">Fee details (data attributes)</button>
<button class="btn btn-outline-primary" id="open-jq">Open with jQuery</button>
<button class="btn btn-outline-secondary" id="open-js">Open with plain JS</button>
<ul id="log" class="mt-3"></ul>
<div class="modal fade" id="fee" tabindex="-1" aria-labelledby="fee-title" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title fs-5" id="fee-title">Hostel fee, Lucknow campus</h2>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">₹6,500 per month, meals included.</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/jquery@4.0.0/dist/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"></script>
<script>
// Bootstrap 5 adds its jQuery plugins when the page is ready, so wait for it
$(function () {
$("#open-jq").on("click", function () { $("#fee").modal("show"); });
$("#fee").on("shown.bs.modal", function () { $("<li>").text("Modal shown").appendTo("#log"); });
});
// The same without jQuery: this is the code that survives the migration
document.querySelector("#open-js").addEventListener("click", () => {
bootstrap.Modal.getOrCreateInstance("#fee").show();
});
</script>
</body>
</html>Bootstrap 5 registers its jQuery plugins only once the page has loaded, which is why the jQuery calls sit inside $(function () { ... }). One more trap from our testing: if a page loads both jQuery UI and Bootstrap, both define $.fn.tooltip (and jQuery UI also has button), so whichever file loads last wins. Bootstrap's components offer .noConflict() to rename theirs.
Common mistakes
- Loading a second jQuery in WordPress. Enqueue with the
jquerydependency and use the bundled copy. - Writing
$(...)at the top level of a WordPress script. UsejQuery(function ($) { ... })or(function ($) { ... })(jQuery). - Skipping the nonce check in
admin-ajax.phphandlers. Always callcheck_ajax_referer()and sanitise every input. - Loading Bootstrap 4's JavaScript before jQuery, or
bootstrap.min.jswithout Popper for dropdowns. Usebootstrap.bundle.min.js. - Upgrading a Bootstrap 4 page to jQuery 4 without testing. It is outside Bootstrap 4's supported range.
Key points
- WordPress ships its own jQuery (3.7.1 in WordPress 7.1) in noConflict mode, so
$is not defined globally. - Enqueue scripts with the
jquerydependency, pass data withwp_localize_script(), and wrap code injQuery(function ($) { ... }). admin-ajax.phprequests send anactionand a nonce; the PHP handler checks it and replies withwp_send_json_success().- Bootstrap 4 components are jQuery plugins (version 4 reached end of life on 1 January 2023); keep them on jQuery 3.7.1.
- Bootstrap 5 needs no jQuery, uses
data-bs-*attributes, and still offers its jQuery interface while you migrate.
Exercise
A WordPress theme file contains $(document).ready(function () { $(".faq-q").click(function () { $(this).next().slideToggle(); }); }); and the console shows $ is not a function. Rewrite it for a WordPress site so that it works with the bundled jQuery, uses .on() with delegation from .faq, and works for FAQ items added later. Then write the wp_enqueue_script() call for a file js/faq.js in a child theme.
Show solution
// js/faq.js
jQuery(function ($) {
$(".faq").on("click", ".faq-q", function () {
$(this).next().slideToggle(200);
});
});add_action('wp_enqueue_scripts', function () {
wp_enqueue_script('child-faq', get_stylesheet_directory_uri() . '/js/faq.js', ['jquery'], '1.0.0', ['in_footer' => true]);
});The jQuery(function ($) { ... }) wrapper waits for the page and gives the code a local $, so noConflict() no longer matters. The ['jquery'] dependency makes WordPress print its bundled jQuery before faq.js, and delegation from .faq covers questions added later.
Quick Quiz
1. Why does $(".menu") throw $ is not a function in a WordPress theme?
Use jQuery(function ($) { ... }) to get $ inside your code.
2. What does the nonce sent with an admin-ajax.php request protect against?
check_ajax_referer() rejects requests without a valid nonce for that action.
3. Which jQuery versions does Bootstrap 4.6.2's package list as supported?
jQuery 4 is outside Bootstrap 4's tested range, and Bootstrap 4 is no longer maintained.
4. In Bootstrap 5, which attribute opens a modal from HTML?
Bootstrap 5 prefixes its data attributes with bs-.
Learn jQuery with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
