Enqueuing Scripts & Styles
Every WordPress theme and plugin needs to load its own CSS and JavaScript files. It's tempting to
just paste a <script> or <link> tag straight into
header.php, but WordPress provides a proper API for this — wp_enqueue_script()
and wp_enqueue_style() — and skipping it causes real problems once a site has more
than one plugin or theme active at a time.
1. The Wrong Way
It's common to see beginner themes do this directly inside a template file:
<!-- Inside header.php — don't do this -->
<link rel="stylesheet" href="/wp-content/themes/mytheme/main.css">
<script src="/wp-content/themes/mytheme/main.js"></script>
This works until it doesn't: the path is hardcoded and breaks if the theme folder is ever renamed, there's no way for another plugin to know this script already exists (so it might load a second, conflicting copy of the same library), and there's no control over when in the page the script loads.
2. The Correct Way: wp_enqueue_script() and wp_enqueue_style()
Instead, hook a function onto the wp_enqueue_scripts action inside
functions.php and register your assets through the enqueue functions:
<?php
function mytheme_assets() {
// Theme's own stylesheet
wp_enqueue_style(
'mytheme-style', // handle: a unique name
get_stylesheet_directory_uri() . '/style.css', // source path
[], // dependencies
'1.0.0' // version (for cache busting)
);
// Custom JS file, loaded in the footer
wp_enqueue_script(
'mytheme-main',
get_stylesheet_directory_uri() . '/js/main.js',
['jquery'], // depends on WP's bundled jQuery
'1.0.0',
true // true = load in the footer
);
}
add_action('wp_enqueue_scripts', 'mytheme_assets');
| Parameter | Meaning |
|---|---|
$handle | A unique name WordPress and other plugins use to refer to this asset |
$src | The file's URL — usually built with get_stylesheet_directory_uri() so it works no matter where the theme is installed |
$deps | An array of handles this asset depends on; WordPress loads those first |
$ver | A version string appended to the URL, so browsers fetch a fresh copy after an update instead of using a stale cached one |
$in_footer | (Scripts only) whether to print the <script> tag in the footer instead of the head, so it doesn't block page rendering |
3. Why This Matters: Dependency Management
The real payoff of enqueuing is that WordPress tracks every registered handle across the entire
site — theme and every active plugin. If two different plugins both need jQuery, and both list
'jquery' in their $deps array, WordPress loads its own bundled jQuery
exactly once and lets both plugins depend on that single copy. Nobody has to bundle their own
copy of jQuery, and there's no risk of two conflicting versions loading on the same page.
jquery, so just list the handle name as a dependency instead of enqueuing your own copy from a CDN.4. Passing PHP Data to JavaScript with wp_localize_script()
A very common real-world need: a script has to know the site's AJAX URL, or needs a security
nonce to include with its request. Since JavaScript can't reach into PHP directly,
wp_localize_script() attaches a small block of PHP data to an already-enqueued script
as a global JavaScript object.
<?php
function mytheme_assets() {
wp_enqueue_script('mytheme-main', get_stylesheet_directory_uri() . '/js/main.js', ['jquery'], '1.0.0', true);
wp_localize_script(
'mytheme-main', // the handle to attach data to
'MyThemeData', // the JS object name created
[
'ajax_url' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('mytheme_ajax_nonce'),
]
);
}
add_action('wp_enqueue_scripts', 'mytheme_assets');
// In main.js, MyThemeData is now available globally:
fetch(MyThemeData.ajax_url + '?action=my_action&_wpnonce=' + MyThemeData.nonce);
wp_verify_nonce() before acting on the request — see Security Best Practices for the full pattern.5. Conditionally Enqueueing Assets
Loading every plugin's CSS and JS on every single page slows the whole site down. If a script or stylesheet is only needed on one page or template, check for that condition before calling the enqueue functions:
<?php
function contact_page_assets() {
if (is_page('contact')) {
wp_enqueue_style('contact-form-styles', get_stylesheet_directory_uri() . '/css/contact.css');
wp_enqueue_script('contact-form-js', get_stylesheet_directory_uri() . '/js/contact.js', [], '1.0.0', true);
}
}
add_action('wp_enqueue_scripts', 'contact_page_assets');
is_page() is one of many WordPress conditional tags — others include
is_single(), is_home(), and is_archive() — that let you scope
assets to exactly the templates that need them, keeping every other page lean.