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');
ParameterMeaning
$handleA unique name WordPress and other plugins use to refer to this asset
$srcThe file's URL — usually built with get_stylesheet_directory_uri() so it works no matter where the theme is installed
$depsAn array of handles this asset depends on; WordPress loads those first
$verA 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.

Tip: WordPress already registers common libraries like 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);
Note: That nonce should be verified server-side with 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.

Next up: Once assets load correctly, the next step is connecting WordPress to the outside world — The REST API covers exposing and consuming WordPress data as JSON.