Enqueuing scripts and styles

Load CSS and JavaScript through WordPress so dependencies, versions and loading strategies are handled for you.

  • Intermediate
  • 9 min read
  • Updated 17 Oct 2026

What you will learn

  • Enqueue assets correctly
  • Declare dependencies and cache-busting versions
  • Load scripts with defer or async strategies
  • Load assets only where needed
  • Pass data from PHP to JavaScript safely

Before you start

Typing <script> and <link> tags into a template seems quicker, but it skips the system every plugin and theme relies on to avoid loading the same library twice, load files in the right order, and let others change or remove your assets. In WordPress you enqueue assets: you tell WordPress what you need, and it prints the tags in the right place.

The basic pattern

Enqueue front-end assets from a callback on the wp_enqueue_scripts action. Despite the name, it is the hook for styles too.

In a plugin, the main file might look like this:

PHP
<?php
/**
 * Plugin Name: My Plugin
 * Version:     1.2.0
 * Text Domain: myplugin
 */

defined( 'ABSPATH' ) || exit;

define( 'MYPLUGIN_VERSION', '1.2.0' );

add_action( 'wp_enqueue_scripts', 'myplugin_enqueue_assets' );

function myplugin_enqueue_assets(): void {
    wp_enqueue_style(
        'myplugin-events',
        plugins_url( 'assets/css/events.css', __FILE__ ),
        array(),
        MYPLUGIN_VERSION
    );

    wp_enqueue_script(
        'myplugin-events',
        plugins_url( 'assets/js/events.js', __FILE__ ),
        array(),
        MYPLUGIN_VERSION,
        array(
            'in_footer' => true,
            'strategy'  => 'defer',
        )
    );
}

The parameters, in order:

ParameterMeaning
HandleA unique name, prefixed with your project, such as myplugin-events. Others use it to depend on or dequeue your asset
SourceThe full URL of the file. Use plugins_url() in plugins and get_theme_file_uri() in themes
DependenciesAn array of handles that must load first
VersionAdded as ?ver= to the URL, so browsers fetch a new copy when it changes
Fifth argumentFor styles, the media type (all by default). For scripts, an array with in_footer and strategy

Styles and scripts have separate registries, so a style and a script can share a handle.

In a theme, the same function goes in functions.php and builds URLs from the theme folder:

PHP
<?php

add_action( 'wp_enqueue_scripts', 'mytheme_enqueue_assets' );

function mytheme_enqueue_assets(): void {
    wp_enqueue_style(
        'mytheme-style',
        get_stylesheet_uri(),
        array(),
        wp_get_theme()->get( 'Version' )
    );
}

get_stylesheet_uri() returns the URL of the active theme's style.css. Block themes often load little or no CSS this way, because theme.json generates most styles; the block themes course covers that.

Dependencies

List the handles your file needs, and WordPress loads them first, once, no matter how many plugins ask for them.

PHP
<?php

wp_enqueue_script(
    'myplugin-calendar',
    plugins_url( 'assets/js/calendar.js', __FILE__ ),
    array( 'myplugin-events', 'wp-i18n' ),
    MYPLUGIN_VERSION,
    array( 'strategy' => 'defer' )
);

WordPress registers many libraries under known handles, such as jquery and the wp- packages used by the block editor (wp-api-fetch, wp-i18n, wp-element). Depend on those handles rather than bundling your own copy.

Do not deregister core's jQuery to load a different version from a CDN. Other plugins and the admin depend on the bundled version, and swapping it breaks things in ways that are hard to trace.

Versions and cache busting

The version is part of the URL, so browsers and CDNs treat a new version as a new file. Use the plugin's version constant in production. During development you can use the file's modification time so every save busts the cache:

PHP
<?php

$path = plugin_dir_path( __FILE__ ) . 'assets/js/events.js';

wp_enqueue_script(
    'myplugin-events',
    plugins_url( 'assets/js/events.js', __FILE__ ),
    array(),
    (string) filemtime( $path ),
    array( 'strategy' => 'defer' )
);

If you build JavaScript with @wordpress/scripts, the build writes an index.asset.php file that returns the dependencies and a content hash. Load it with require and pass both values to wp_enqueue_script(). The Gutenberg development course uses this workflow.

Scripts in the <head> block page rendering. The fifth argument for scripts lets you avoid that:

  • 'in_footer' => true prints the tag before </body>.
  • 'strategy' => 'defer' adds the defer attribute: the script downloads in parallel and runs in order after the HTML is parsed. This is the right choice for most scripts.
  • 'strategy' => 'async' runs the script as soon as it downloads, in any order. Use it only for independent scripts such as analytics.

WordPress checks the dependency tree. If a deferred script is a dependency of a script that cannot be deferred, WordPress falls back to normal loading so nothing runs out of order. You get the benefit when it is safe and correctness when it is not.

Current versions of WordPress also support native JavaScript modules through wp_register_script_module() and wp_enqueue_script_module(). The Interactivity API uses them. Classic scripts remain the common case for plugin code.

Load assets only where needed

Every file you enqueue on every page costs every visitor download time. Check where you are before enqueuing:

PHP
<?php

add_action( 'wp_enqueue_scripts', 'myplugin_enqueue_event_assets' );

function myplugin_enqueue_event_assets(): void {
    if ( ! is_singular( 'myplugin_event' ) && ! is_post_type_archive( 'myplugin_event' ) ) {
        return;
    }

    wp_enqueue_style(
        'myplugin-events',
        plugins_url( 'assets/css/events.css', __FILE__ ),
        array(),
        MYPLUGIN_VERSION
    );
}

Conditional tags such as is_singular() work here because wp_enqueue_scripts fires after the main query has run.

Register now, enqueue later

For assets used by a shortcode, you cannot know in advance whether the page uses it. Register the asset early, then enqueue it from the shortcode callback:

PHP
<?php

add_action( 'wp_enqueue_scripts', 'myplugin_register_assets' );

function myplugin_register_assets(): void {
    wp_register_script(
        'myplugin-countdown',
        plugins_url( 'assets/js/countdown.js', __FILE__ ),
        array(),
        MYPLUGIN_VERSION,
        array( 'in_footer' => true )
    );
}

add_shortcode( 'myplugin_countdown', 'myplugin_countdown_shortcode' );

function myplugin_countdown_shortcode(): string {
    wp_enqueue_script( 'myplugin-countdown' );

    return '<div class="myplugin-countdown"></div>';
}

Because the shortcode runs while the content is being rendered, after the <head> has been printed, the script can only be printed in the footer. WordPress handles that, and registering it with in_footer makes the intent explicit. Late-enqueued styles also print in the footer, which can cause a flash of unstyled content, so keep them small or register them with a conditional check instead.

For blocks, block.json declares scripts and styles and WordPress loads them only on pages that contain the block. That is covered in the Gutenberg development course.

Admin assets

Admin pages use a different hook, admin_enqueue_scripts, which receives the current admin page's hook suffix. Use it to load assets only on your own screen:

PHP
<?php

add_action( 'admin_enqueue_scripts', 'myplugin_admin_assets' );

function myplugin_admin_assets( string $hook_suffix ): void {
    if ( 'settings_page_myplugin' !== $hook_suffix ) {
        return;
    }

    wp_enqueue_style(
        'myplugin-admin',
        plugins_url( 'assets/css/admin.css', __FILE__ ),
        array(),
        MYPLUGIN_VERSION
    );
}

add_options_page() returns the hook suffix when you register a settings page, so you can store it rather than guessing the string.

Passing data from PHP to JavaScript

Scripts often need values from PHP: the REST API URL, a nonce, a translated label. Add them as an inline script that runs before your file, encoded with wp_json_encode():

PHP
<?php

$data = array(
    'restUrl' => esc_url_raw( rest_url( 'myplugin/v1/' ) ),
    'nonce'   => wp_create_nonce( 'wp_rest' ),
    'perPage' => 12,
);

wp_add_inline_script(
    'myplugin-events',
    'const mypluginData = ' . wp_json_encode( $data ) . ';',
    'before'
);
JavaScript
// assets/js/events.js
fetch( mypluginData.restUrl + 'events?per_page=' + mypluginData.perPage )
    .then( ( response ) => response.json() )
    .then( ( events ) => console.log( events ) );

wp_json_encode() produces valid JavaScript for strings, numbers, booleans and arrays, and escapes characters safely. Never build JavaScript by concatenating raw PHP values into a string.

You will also see wp_localize_script() used for this. It was designed for translated strings, converts every scalar to a string, and is still widely used. For new code, wp_add_inline_script() with wp_json_encode() keeps types intact and is clearer. The nonce and REST lessons that follow explain what the wp_rest nonce is for.

wp_add_inline_style() does the same for CSS, which is handy for a colour chosen in the settings. Sanitise such values before you output them, as the sanitising lesson explains.

Summary

  • Enqueue assets on wp_enqueue_scripts (front end) or admin_enqueue_scripts (admin); never hard-code tags in templates.
  • Give each asset a prefixed handle, its dependencies and a version for cache busting.
  • Use 'strategy' => 'defer' for most scripts; WordPress falls back safely when dependencies require it.
  • Load assets only on pages that need them, and register early then enqueue on demand for shortcodes.
  • Pass data to scripts with wp_add_inline_script() and wp_json_encode().

Further reading