Enqueuing CSS & JS

wp_enqueue_style and wp_enqueue_script register assets with handles, dependencies and versions so WordPress can order and dedupe them.

Syntaxwp_enqueue_script( string $handle, string $src = '', string[] $deps = array(), string|bool|null $ver = false, array|bool $args = array() )

WordPress owns the <link> and <script> tags on the page. You add to a queue; core resolves dependency order, prints each handle once, and gives plugins a chance to move, replace or remove your assets. Hardcoding a tag in header.php bypasses all of that, and is why two copies of jQuery end up on so many sites.

The hooks

Enqueue on wp_enqueue_scripts for the front end, admin_enqueue_scripts for wp-admin, login_enqueue_scripts for the login screen, and enqueue_block_editor_assets for the editor. Never call these functions at the top level of functions.php.

Handles, dependencies, versions

The handle must be unique site-wide. $deps is an array of other handles that must load first. $ver is appended as a query string for cache busting: false (the default) uses the WordPress version, null appends nothing, and a string is used verbatim — filemtime() is the reliable choice.

Placement

Since WordPress 6.3 the fifth argument to wp_enqueue_script() is an $args array accepting in_footer and strategy ('defer' or 'async'). Passing a plain boolean still works and still means in_footer.

Example

Example · php
<?php
/**
 * Front-end assets.
 */
function mytheme_assets() {
	$dir = get_stylesheet_directory();     // filesystem path
	$uri = get_stylesheet_directory_uri(); // public URL
	$css = '/assets/css/main.css';
	$js  = '/assets/js/main.js';

	wp_enqueue_style(
		'mytheme-main',            // unique handle
		$uri . $css,
		array(),                   // $deps: style handles to load first
		filemtime( $dir . $css ),  // $ver: mtime changes on every deploy
		'all'                      // $media
	);

	wp_enqueue_script(
		'mytheme-main',
		$uri . $js,
		array( 'wp-i18n' ),        // core's i18n package loads first
		filemtime( $dir . $js ),
		array(
			'in_footer' => true,
			'strategy'  => 'defer', // $args array form: WP 6.3+
		)
	);

	// Hand PHP values to JS instead of echoing an inline <script>.
	wp_localize_script(
		'mytheme-main',
		'MyThemeData',             // becomes window.MyThemeData
		array(
			'restUrl' => esc_url_raw( rest_url( 'wp/v2/' ) ),
			'nonce'   => wp_create_nonce( 'wp_rest' ),
		)
	);

	// Comment reply script only where it is needed.
	if ( is_singular() && comments_open() && get_option( 'thread_comments' ) ) {
		wp_enqueue_script( 'comment-reply' ); // core handle, no $src needed
	}
}
add_action( 'wp_enqueue_scripts', 'mytheme_assets' );

When to use it

  • Shipping a theme's CSS and JS so plugins can depend on or override them
  • Busting the browser cache on deploy without renaming files
  • Loading a heavy script only on the templates that use it
  • Removing a plugin's stylesheet from pages that do not need it

More examples

Footer shorthand and its longhand

Registering without enqueuing declares an asset so other code can depend on it or turn it on conditionally, without ever printing it. This is how you expose a shared library from a plugin and let themes opt in.

Example · php
<?php
// These two are the same call. The boolean form predates WP 6.3 and is not
// deprecated — core still reads a bool $args as in_footer.
wp_enqueue_script( 'mytheme-app', $src, array(), '1.0.0', true );
wp_enqueue_script( 'mytheme-app', $src, array(), '1.0.0', array( 'in_footer' => true ) );

// Only the array form can also set a loading strategy (WP 6.3+).
wp_enqueue_script( 'mytheme-app', $src, array(), '1.0.0', array(
	'in_footer' => true,
	'strategy'  => 'defer', // or 'async'
) );

// Register now, enqueue later — the longhand of a single enqueue call.
wp_register_script( 'mytheme-map', $src, array(), '1.0.0', array( 'in_footer' => true ) );
// …elsewhere, once you know the page needs it:
wp_enqueue_script( 'mytheme-map' ); // handle only; src/deps come from the registration

Conditional and admin enqueues

get_theme_file_uri() (WP 4.7+) checks the child theme directory before the parent, so a child can drop in its own asset file without touching PHP. wp_get_theme()->get('Version') reads the Version header from style.css, which is a decent cache-buster when you bump it on every release.

Example · php
<?php
// Only load the gallery bundle on single posts that have a gallery block.
add_action( 'wp_enqueue_scripts', function () {
	if ( is_singular() && has_block( 'core/gallery', get_queried_object_id() ) ) {
		wp_enqueue_script(
			'mytheme-lightbox',
			get_theme_file_uri( 'assets/js/lightbox.js' ), // child theme wins if present
			array(),
			wp_get_theme()->get( 'Version' ),
			array( 'in_footer' => true )
		);
	}
} );

// admin_enqueue_scripts passes the current admin page hook suffix.
add_action( 'admin_enqueue_scripts', function ( $hook_suffix ) {
	if ( 'post.php' !== $hook_suffix && 'post-new.php' !== $hook_suffix ) {
		return;
	}
	wp_enqueue_style( 'mytheme-admin', get_theme_file_uri( 'assets/css/admin.css' ) );
} );

Replacing or removing someone else's asset

Dequeue removes an asset from this request; deregister forgets it entirely, which also breaks anything that declared it as a dependency. Priority matters: unhooking at the default priority 10 often runs before the plugin has enqueued, and appears to do nothing.

Example · php
<?php
// Run late so the plugin has already enqueued before you unhook it.
add_action( 'wp_enqueue_scripts', function () {
	if ( ! is_page( 'contact' ) ) {
		wp_dequeue_style( 'some-plugin-css' );  // remove from the queue
		wp_deregister_style( 'some-plugin-css' ); // and drop the registration
	}
}, 100 ); // default priority is 10; the plugin enqueued at 10

// Add a rule to an already-enqueued handle rather than shipping another file.
add_action( 'wp_enqueue_scripts', function () {
	wp_add_inline_style( 'mytheme-main', ':root{--gutter:1.5rem}' );
}, 20 );

Discussion

  • Be the first to comment on this lesson.