
🛠 Proper JavaScript inclusion in WordPress themes
Your WordPress gallery stopped working after an update. Sound familiar?
Almost always the cause is JavaScript linked directly in header.php, without hooks, without dependencies, without accounting for the fact that a caching plugin can shuffle the load order. One plugin pulls in jQuery, another pulls its own version of jQuery, and your script ends up between them and crashes with $ is not defined.
In practice, a single wp_enqueue_script call in the right place is enough to forget about conflicts forever. Below is a complete breakdown: from beginner mistakes to defer/async strategies and fine-tuning options that appeared in WordPress 6.6.
💡 Quick overview:
- Don't insert script tags directly into header.php or footer.php: this creates conflicts with plugins and breaks child themes
- Enqueue JavaScript through wp_enqueue_script in functions.php with the wp_enqueue_scripts hook: WordPress will place the tags in head or before /body itself
- For short inline code use wp_add_inline_script, not a bare script tag: preserves execution order and gives plugins a chance to intercept the code
- On WordPress 6.3+ specify a defer or async strategy directly in the call parameters, and starting with 6.6, fetchpriority as well
- Dequeue third-party scripts through wp_dequeue_script if a plugin slows down your site: hook priority decides
How JavaScript is incorrectly added to WordPress
Typical scenario: you need to load your custom.js, and the developer inserts this line in header.php:
1 <script src="<?php echo get_template_directory_uri(); ?>/js/custom.js"></script>
Seems to work. But this approach creates three problems.
Conflicts with plugins. A caching plugin combines scripts in a different order, a minification plugin renames the file and breaks the path. If another plugin already loaded jQuery and you load it again, double loading and console errors.
Can't override through a child theme. When using a child theme, header.php is rarely copied. If the script is hardcoded into the parent header.php, the child theme can't dequeue or replace it without copying the entire file, and this means edits will be lost when the parent theme updates.
No dependencies. The browser loads your script before jQuery, and you get $ is not defined. The script loads on all pages, even where it's not needed, and extra requests slow down the site.
The right way: wp_enqueue_script
WordPress provides a queue system for scripts and styles. You don't insert <script> manually, instead you register the script through PHP, specify dependencies and load location. WordPress places the tags in <head> or before </body> itself.
Minimal working example for your theme's functions.php:
1 /** 2 * Enqueue the main theme script. 3 */ 4 function mytheme_enqueue_scripts() { 5 wp_enqueue_script( 6 'mytheme-main', // $handle — unique name 7 get_template_directory_uri() . '/js/main.js', // $src — file path 8 array( 'jquery' ), // $deps — dependencies 9 '1.0.0', // $ver — version for cache busting 10 true // $in_footer — load in footer 11 ); 12 } 13 add_action( 'wp_enqueue_scripts', 'mytheme_enqueue_scripts' );
The wp_enqueue_script function accepts five parameters. $handle, a unique identifier: if another plugin already registered a script with the same handle, there will be no duplicate loading. $deps, an array of handle dependencies: WordPress will load the required scripts before yours. $in_footer with a value of true places the <script> tag before </body>, which speeds up page rendering.
Note: get_template_directory_uri() points to the parent theme folder. In a child theme use get_stylesheet_directory_uri(), the URL will point to the child folder, and your script won't be lost when the parent theme updates.
Load strategies: defer, async and fetchpriority
Starting with WordPress 6.3, the $args parameter accepts an array with additional settings:
1 wp_enqueue_script( 2 'mytheme-main', 3 get_template_directory_uri() . '/js/main.js', 4 array( 'jquery' ), 5 '1.0.0', 6 array( 7 'in_footer' => true, 8 'strategy' => 'defer', 9 ) 10 );
The defer strategy tells the browser: "load the script in parallel, execute after DOM construction". Execution order is guaranteed, scripts with defer execute in the order they were added to the DOM. The async strategy means "execute as soon as loaded", order is not guaranteed. For theme scripts that depend on the DOM, choose defer.
Starting with WordPress 6.6, the $args array gained two more parameters. fetchpriority controls load priority ('high' or 'low'), useful for critical scripts that should load before others. module_dependencies accepts an array of module IDs for dynamic import, this is for advanced scenarios with ES modules. Important: when specifying module_dependencies the script must load in the footer (in_footer => true) or with the defer strategy, otherwise the module import map won't be ready when the script executes.
What scripts are already in WordPress
WordPress registers dozens of JavaScript libraries out of the box: jQuery, jQuery UI, Backbone, wp-api, MediaElement.js and others. Full list in the official documentation.
The main rule: never load jQuery from a CDN like Google or cdnjs if your theme works in the WordPress ecosystem. WordPress itself provides jQuery with the handle jquery. To use it, simply specify array( 'jquery' ) in dependencies, WordPress will load its own version tested for compatibility with the entire core.
You can check if a library is registered through wp_script_is():
1 if ( wp_script_is( 'jquery-ui-datepicker', 'registered' ) ) { 2 // Library is available — just add to $deps 3 }
Using the wp_enqueue_scripts hook
The wp_enqueue_scripts hook fires on the frontend. This is what wp_enqueue_script calls are wrapped in. Don't call the function directly in the body of functions.php without a hook, the script might load before WordPress registers system libraries.
For the admin area use a separate hook:
1 add_action( 'admin_enqueue_scripts', 'mytheme_admin_scripts' ); 2 function mytheme_admin_scripts( $hook_suffix ) { 3 // $hook_suffix contains the current admin page 4 if ( 'post.php' !== $hook_suffix ) { 5 return; // load script only on the post edit page 6 } 7 wp_enqueue_script( 'mytheme-admin', get_template_directory_uri() . '/js/admin.js', array(), '1.0', true ); 8 }
Conditional loading saves resources: a script for an options page shouldn't load on all admin pages. The $hook_suffix parameter is passed automatically by WordPress, use it.
If a script is needed both on the frontend and in admin, hook one function to both:
1 add_action( 'wp_enqueue_scripts', 'mytheme_global_scripts' ); 2 add_action( 'admin_enqueue_scripts', 'mytheme_global_scripts' );
Adding inline JavaScript: wp_add_inline_script
Not all code is worth putting in a separate file. For short snippets, counters, config variables, quick handlers, WordPress provides wp_add_inline_script:
1 function mytheme_inline_config() { 2 wp_enqueue_script( 'mytheme-main', get_template_directory_uri() . '/js/main.js', array(), '1.0', true ); 3 wp_add_inline_script( 4 'mytheme-main', 5 'const MYTHEME_AJAX_URL = "' . admin_url( 'admin-ajax.php' ) . '";', 6 'before' 7 ); 8 } 9 add_action( 'wp_enqueue_scripts', 'mytheme_inline_config' );
The third parameter, 'before' or 'after', determines where the code is inserted relative to the specified script. This is convenient for passing PHP variables to JavaScript: the AJAX handler URL, security nonce key, current post ID.
Important detail: wp_add_inline_script only works with a registered script. If you pass a handle that's not in the queue, the code won't output. So first wp_enqueue_script, then wp_add_inline_script.
Alternative for child themes: wp_head and wp_footer
If you're working in a child theme and don't want to create a separate file, you can output code directly through the wp_head (in <head>) or wp_footer (before </body>) hooks:
1 add_action( 'wp_footer', function() { ?> 2 <script> 3 ( function( $ ) { 4 'use strict'; 5 $( function() { 6 // Your code here — DOM is already ready 7 } ); 8 } ( jQuery ) ); 9 </script> 10 <?php } );
This method is shorter but less flexible than wp_enqueue_script: no dependencies, versioning or ability to dequeue the script through a child theme. Use it for small fixes when you don't want to create a separate file.
How to dequeue a script added by a plugin or theme
Sometimes a plugin loads an unnecessary script on all pages and slows down the site. You can remove it through wp_dequeue_script:
1 function mytheme_dequeue_plugin_scripts() { 2 if ( ! is_page( 'contacts' ) ) { 3 wp_dequeue_script( 'plugin-handle' ); 4 } 5 } 6 add_action( 'wp_enqueue_scripts', 'mytheme_dequeue_plugin_scripts', 20 );
Priority 20 (third argument of add_action) places your function after the plugin registration, otherwise the handle won't be in the queue yet. You can find a script's handle through the browser console: open the page source and find the id attribute on the <script> tag, the handle usually matches the id without the -js suffix.
If you need to completely replace a script, first deregister the old one through wp_deregister_script, then register the new one:
1 function mytheme_replace_script() { 2 wp_deregister_script( 'old-handle' ); 3 wp_enqueue_script( 'old-handle', get_template_directory_uri() . '/js/replacement.js', array(), '2.0', true ); 4 } 5 add_action( 'wp_enqueue_scripts', 'mytheme_replace_script', 20 );
Before such operations make a complete site backup. Replacing plugin scripts is a direct path to broken functionality if you don't test in a staging environment.
The video above shows a step-by-step breakdown of enqueuing CSS and JavaScript in a WordPress theme through wp_enqueue_script and wp_enqueue_style. If you're new to hooks, start with it, then return to this written guide for details.
⁉️🤔 Frequently asked questions
Is it mandatory to use wp_enqueue_script for every js file?
In practice, yes, for all globally loaded scripts. For a script that works on one single page and is generated dynamically, it's acceptable to insert
<script>directly in the template. But as soon as you have two or more scripts, the queue throughwp_enqueue_scriptsaves hours of debugging.
How does wp_register_script differ from wp_enqueue_script?
wp_register_scriptonly registers a script in the system (sets handle, path, dependencies) but doesn't output the tag on the page.wp_enqueue_scriptregisters AND immediately queues for output. The separation is useful when the script isn't always needed: register once infunctions.php, and callwp_enqueue_script('my-handle')only on the required pages.
Can I load scripts from an external CDN?
Technically, yes, pass the full URL as the second parameter. But for jQuery and other core WordPress libraries this is bad practice: WordPress already includes them and tests compatibility. An external CDN is justified for third-party services like Google Analytics, reCAPTCHA, chat widgets. There specify the URL as is.
How do I verify that a script actually loaded?
Open developer tools (F12), Network tab, filter by JS and refresh the page. Your file should be in the list with a 200 status code. If the script is missing, check that the
wp_enqueue_scriptshook fires on this page and that the file path is correct. For debugging, outputget_template_directory_uri()separately and compare the URL.
What should I do if scripts stop working after a theme update?
Most likely the update overwrote your
functions.php. The solution is a child theme: create one, move your functions there and activate it. After that, parent theme updates will leave your code intact.
What does the error "$ is not defined" mean when using jQuery?
WordPress loads jQuery in noConflict mode, the
$variable is not occupied by jQuery to avoid conflicts with other libraries. Use the full namejQueryinstead of$or wrap the code in a self-invoking function:(function($) { ... })(jQuery);.
Is it worth bothering with wp_enqueue_script for a couple of scripts?
If you have more than one plugin on the site, it's worth it. WordPress script queue is designed so that one plugin loading jQuery directly breaks half the site the moment you install a caching plugin. wp_enqueue_script eliminates this entire class of problems.
For a five-file theme, use wp_enqueue_script. For one short snippet on a specific page, wp_add_inline_script or the wp_footer hook will do. But as soon as you have two or more scripts, return to the queue. An hour spent on proper enqueuing now saves an evening of debugging after the next WordPress update.



