Every WordPress developer hits the same wall early on: scripts load in the wrong order, jQuery gets duplicated, or a plugin’s CSS bleeds into every page of a site. All of those problems trace back to the same root cause — bypassing WordPress’s built-in asset management system.
WordPress ships with a dependency-aware queue for scripts and stylesheets. Use it correctly and the platform handles load order, version-based cache busting, and conditional loading automatically. Skip it and you’re managing those concerns manually, one conflict at a time.
This guide covers everything: wp_enqueue_script(), wp_enqueue_style(), registering versus enqueueing, passing PHP data to JavaScript with wp_localize_script(), conditional loading, dequeuing plugin assets, and loading assets in the admin and block editor.
Why WordPress Has an Asset Queue
When two plugins both output a raw <script src="jquery.min.js"> tag, the browser loads jQuery twice — different versions, different cache entries, potential conflicts. WordPress’s enqueue system solves this by treating every script and stylesheet as a named entry in a dependency graph.
- Deduplication: A handle registered once loads once, even if ten plugins request it.
- Dependency ordering: Declare that your script depends on
jqueryand WordPress ensures jQuery is in the DOM before your code runs. - Cache busting: A version string becomes a query parameter (
?ver=1.4.2), so browsers invalidate the cache when you ship a new file. - Conditional removal: Any script in the queue can be dequeued by a later hook before the page renders — impossible with hardcoded tags.
The correct action hook for frontend assets is wp_enqueue_scripts (note the plural):
function my_theme_assets() {
// All wp_enqueue_script() and wp_enqueue_style() calls go here
}
add_action( 'wp_enqueue_scripts', 'my_theme_assets' );
wp_enqueue_script(): Every Argument Explained
wp_enqueue_script( $handle, $src, $deps, $ver, $in_footer );
$handle — the unique name
A lowercase slug that identifies this script across the whole WordPress installation. Always prefix it (mytheme-, myplugin-) to avoid colliding with core or third-party handles. This handle is what you pass to wp_dequeue_script() or reference in a dependency array.
$src — the URL
Use get_template_directory_uri() for theme files and plugin_dir_url( __FILE__ ) for plugin files. Never hardcode a domain.
$deps — the dependency array
WordPress resolves this before printing any <script> tags. If your slider needs jQuery, write array( 'jquery' ); WordPress guarantees jQuery loads first. If jQuery isn’t already queued, WordPress queues it automatically.
$ver — version string for cache busting
Use filemtime() during development (busts cache on every file save) and a pinned semantic version in production:
// Development: auto-bust on every file change
$ver = filemtime( get_template_directory() . '/js/main.js' );
// Production: pinned version from a constant
$ver = MY_THEME_VERSION;
$in_footer — load position
Pass true to print the tag just before </body>. This is the single easiest performance win in WordPress asset management: the browser can paint the page before it parses your JavaScript. Only pass false (the default) when a script genuinely must run before the first element renders.
WP 6.3+: defer and async via the $args array
From WordPress 6.3 onward, the fifth argument can be an array instead of a boolean. This unlocks native defer and async attributes without monkey-patching script_loader_tag:
wp_enqueue_script(
'mytheme-main',
get_template_directory_uri() . '/js/main.js',
array( 'jquery' ),
MYTHEME_VERSION,
array(
'in_footer' => true,
'strategy' => 'defer', // or 'async'
)
);
Use strategy: 'defer' for scripts that depend on the DOM. Use strategy: 'async' only for fully independent scripts (analytics, ad tags) that have no dependencies in the queue.
wp_enqueue_style(): Loading Stylesheets
wp_enqueue_style( $handle, $src, $deps, $ver, $media );
The arguments map directly to the script equivalent, except the fifth parameter is $media (e.g. 'all', 'screen', 'print') rather than a footer flag. Stylesheets belong in the <head> — loading them late causes a Flash of Unstyled Content (FOUC).
function mytheme_styles() {
wp_enqueue_style(
'mytheme-main',
get_template_directory_uri() . '/css/main.css',
array(),
filemtime( get_template_directory() . '/css/main.css' ),
'all'
);
// Component stylesheet that depends on the base reset
wp_enqueue_style(
'mytheme-slider',
get_template_directory_uri() . '/css/slider.css',
array( 'mytheme-main' ),
MYTHEME_VERSION,
'all'
);
}
add_action( 'wp_enqueue_scripts', 'mytheme_styles' );
Style dependencies work the same way as script dependencies. If your component stylesheet requires a base reset, add the reset’s handle to $deps and WordPress keeps them in order.
wp_register_script() vs wp_enqueue_script()
Registering adds a script to the dependency graph without printing a tag. Enqueueing both registers and marks it for output. The distinction matters when you want to register a library in a central location (say, your plugin bootstrap) but only load it on pages that actually need it:
// Register in the main plugin file — available everywhere, loaded nowhere yet
function myplugin_register_assets() {
wp_register_script(
'myplugin-chart',
plugin_dir_url( __FILE__ ) . 'js/chart.min.js',
array(),
'4.4.2',
true
);
}
add_action( 'wp_enqueue_scripts', 'myplugin_register_assets' );
// Only enqueue on pages that render the shortcode
function myplugin_maybe_enqueue_chart() {
if ( is_singular() && has_shortcode( get_post()->post_content, 'sales_chart' ) ) {
wp_enqueue_script( 'myplugin-chart' );
}
}
add_action( 'wp_enqueue_scripts', 'myplugin_maybe_enqueue_chart', 20 );
This pattern is common for JavaScript libraries that power optional features — register once, enqueue conditionally. The same wp_register_style() / wp_enqueue_style() pair exists for stylesheets.
Passing PHP Data to JavaScript
A JavaScript file served from disk cannot read PHP variables at runtime. The two approved ways to bridge the gap are wp_localize_script() for structured data and wp_add_inline_script() for raw JS.
wp_localize_script()
This function serializes a PHP array as a JavaScript object and prints it in a <script> block immediately before the target handle. It must be called after wp_enqueue_script():
function myplugin_enqueue() {
wp_enqueue_script(
'myplugin-main',
plugin_dir_url( __FILE__ ) . 'js/main.js',
array( 'jquery' ),
MYPLUGIN_VERSION,
true
);
wp_localize_script(
'myplugin-main',
'myPluginData', // JS variable name
array(
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'myplugin_ajax' ),
'postId' => get_the_ID(),
)
);
}
add_action( 'wp_enqueue_scripts', 'myplugin_enqueue' );
In your JavaScript file, reference the object by the name you passed as the second argument (myPluginData above):
jQuery.ajax({
url: myPluginData.ajaxUrl,
data: {
action: 'myplugin_action',
nonce: myPluginData.nonce,
postId: myPluginData.postId,
}
});
wp_add_inline_script()
When you need to output raw JavaScript rather than a structured object — for example, to set a configuration flag or call an initializer — use wp_add_inline_script():
wp_add_inline_script(
'myplugin-main',
'window.myPlugin = window.myPlugin || {}; myPlugin.debug = ' . ( WP_DEBUG ? 'true' : 'false' ) . ';',
'before'
);
The third argument is 'before' or 'after' (default), controlling where the inline block appears relative to the script tag.
Conditional Loading for Real Performance Gains
Loading every asset on every page is the most common WordPress performance mistake. WordPress ships with a rich set of conditional tags — use them inside your enqueue callback to restrict each asset to the pages that actually need it:
function mytheme_conditional_assets() {
// Slider only on the homepage
if ( is_front_page() ) {
wp_enqueue_style( 'mytheme-slider', get_template_directory_uri() . '/css/slider.css', array(), MYTHEME_VERSION );
wp_enqueue_script( 'mytheme-slider', get_template_directory_uri() . '/js/slider.js', array( 'jquery' ), MYTHEME_VERSION, true );
}
// Comment reply script only on single posts with comments open
if ( is_singular() && comments_open() ) {
wp_enqueue_script( 'comment-reply' );
}
// Contact form assets only on the contact page
if ( is_page( 'contact' ) ) {
wp_enqueue_script( 'mytheme-contact', get_template_directory_uri() . '/js/contact.js', array(), MYTHEME_VERSION, true );
}
}
add_action( 'wp_enqueue_scripts', 'mytheme_conditional_assets', 20 );
The priority argument on add_action (20 in the example above) ensures conditional checks run after all plugins have had a chance to register their assets at the default priority of 10.
For WooCommerce contexts, use is_woocommerce(), is_cart(), is_checkout(), and is_product() instead of their generic equivalents to avoid false matches on WooCommerce archive pages.
Dequeuing and Deregistering Plugin Assets
Plugins frequently load scripts and stylesheets on pages that don’t use them. Use wp_dequeue_script() or wp_dequeue_style() to remove an asset from the output queue without removing it from the registry (so it can still be depended on). Use wp_deregister_script() / wp_deregister_style() to remove it entirely:
function mytheme_cleanup_plugin_assets() {
// Dequeue Contact Form 7 styles on every page except those with the form
if ( ! is_page( array( 'contact', 'request-quote' ) ) ) {
wp_dequeue_style( 'contact-form-7' );
wp_dequeue_script( 'contact-form-7' );
}
// Swap the bundled jQuery for a CDN version (deregister required for src swap)
wp_deregister_script( 'jquery' );
wp_register_script( 'jquery', 'https://cdn.example.com/jquery-3.7.1.min.js', array(), '3.7.1', true );
}
add_action( 'wp_enqueue_scripts', 'mytheme_cleanup_plugin_assets', 20 );
The priority on add_action must be higher than the plugin’s own enqueue priority — plugins typically use the default of 10, so 20 or higher is usually safe. Check the plugin source if dequeue seems to have no effect; the plugin may be using a late priority itself.
To find a plugin’s handle, inspect the page source and note the id attribute on the <script> tag — WordPress appends -js to the handle, so id="contact-form-7-js" means the handle is contact-form-7.
Enqueueing Assets in the Admin and Block Editor
The wp_enqueue_scripts hook fires only on the front end. For the wp-admin dashboard, use admin_enqueue_scripts; for the Gutenberg block editor specifically, use enqueue_block_editor_assets.
Admin assets
function myplugin_admin_assets( $hook ) {
// Only load on this plugin's settings page
if ( 'settings_page_myplugin' !== $hook ) {
return;
}
wp_enqueue_style(
'myplugin-admin',
plugin_dir_url( __FILE__ ) . 'css/admin.css',
array(),
MYPLUGIN_VERSION
);
wp_enqueue_script(
'myplugin-admin-settings',
plugin_dir_url( __FILE__ ) . 'js/admin-settings.js',
array( 'jquery', 'wp-color-picker' ),
MYPLUGIN_VERSION,
true
);
}
add_action( 'admin_enqueue_scripts', 'myplugin_admin_assets' );
The $hook parameter passed to the callback identifies the current admin page (e.g., post.php, toplevel_page_my-plugin). Use it to scope assets to the screens that need them — loading custom admin CSS on every wp-admin page slows the editor for no reason.
Block editor assets
function myplugin_block_editor_assets() {
wp_enqueue_script(
'myplugin-block-editor',
plugin_dir_url( __FILE__ ) . 'js/block-editor.js',
array( 'wp-blocks', 'wp-element', 'wp-editor', 'wp-components' ),
MYPLUGIN_VERSION,
true
);
wp_enqueue_style(
'myplugin-block-editor-style',
plugin_dir_url( __FILE__ ) . 'css/editor.css',
array( 'wp-edit-blocks' ),
MYPLUGIN_VERSION
);
}
add_action( 'enqueue_block_editor_assets', 'myplugin_block_editor_assets' );
enqueue_block_editor_assets fires inside the block editor context and is the correct hook for custom block scripts or editor-only stylesheets. For assets that should load in the editor and on the front end, use enqueue_block_assets instead.
A Complete Theme Asset Loader
Here is a production-ready theme enqueue setup combining everything above — cache busting, footer loading, inline data, and conditional assets:
define( 'MYTHEME_VERSION', '2.4.1' );
function mytheme_enqueue_assets() {
$dir = get_template_directory();
$uri = get_template_directory_uri();
// Main stylesheet (always loads)
wp_enqueue_style(
'mytheme-main',
$uri . '/css/main.css',
array(),
filemtime( $dir . '/css/main.css' )
);
// Main JS (deferred, depends on jQuery)
wp_enqueue_script(
'mytheme-main',
$uri . '/js/main.js',
array( 'jquery' ),
filemtime( $dir . '/js/main.js' ),
array( 'in_footer' => true, 'strategy' => 'defer' )
);
// Pass REST nonce and site URL to JS
wp_localize_script(
'mytheme-main',
'mythemeData',
array(
'restUrl' => esc_url( rest_url() ),
'nonce' => wp_create_nonce( 'wp_rest' ),
)
);
// Slider: front page only
if ( is_front_page() ) {
wp_enqueue_style( 'mytheme-slider', $uri . '/css/slider.css', array( 'mytheme-main' ), MYTHEME_VERSION );
wp_enqueue_script( 'mytheme-slider', $uri . '/js/slider.js', array( 'jquery' ), MYTHEME_VERSION, true );
}
// Comment reply: single posts with open comments only
if ( is_singular() && comments_open() ) {
wp_enqueue_script( 'comment-reply' );
}
}
add_action( 'wp_enqueue_scripts', 'mytheme_enqueue_assets' );
Checklist: WordPress Asset Enqueueing
- All scripts and styles go through
wp_enqueue_script()/wp_enqueue_style()— no hardcoded tags. - Every handle is prefixed (
mytheme-,myplugin-) to avoid collisions. - Every URL uses
get_template_directory_uri()(themes) orplugin_dir_url( __FILE__ )(plugins). - Dependencies are declared in
$deps— never rely on page order. - Scripts load in the footer (
$in_footer = true) unless there is a specific reason not to. - WP 6.3+: use the
$argsarray form withstrategy: 'defer'instead ofscript_loader_taghacks. - Version strings use
filemtime()during development and a constant in production. - PHP data travels to JavaScript via
wp_localize_script()orwp_add_inline_script(). - Assets are restricted to the pages that need them using conditional tags.
- Plugin assets are removed with
wp_dequeue_at a higher hook priority (20+).
Need help auditing your site’s asset footprint or migrating a theme off hardcoded script tags? Get in touch — I can profile what’s loading, strip the bloat, and wire up conditional loading so each page only pays for what it uses.
For more on managing PHP dependencies in WordPress projects, see the guide on using Composer for WordPress dependencies. If you’re working with external third-party scripts, integrating external JavaScript in WordPress covers the patterns for safely pulling in CDN-hosted libraries.


