Tips・The foundations of WordPress customization

The right way to load CSS and JavaScript (wp_enqueue)

The right way to load CSS and JavaScript in a theme. Covers how to write wp_enqueue_style, version strings for cache busting, using jQuery, and why you shouldn't hard-code tags into header.php.

The right way to load a theme’s CSS and JavaScript is from functions.php, with wp_enqueue_style() / wp_enqueue_script(). Hard-coding <link> into header.php leaves order and duplicates unmanaged, and causes trouble.

function my_assets() {
  wp_enqueue_style('main', get_theme_file_uri('style.css'), [], '1.0.0');
  wp_enqueue_script('main', get_theme_file_uri('js/main.js'), [], '1.0.0', true);
}
add_action('wp_enqueue_scripts', 'my_assets');

That alone outputs them correctly at the positions of wp_head() and wp_footer().

Theme developmentWiring up functions.php and CSS the right wayWant to learn this as part of building a theme? Head to this lesson of the theme development course

What the arguments mean

wp_enqueue_style('handle', 'URL', dependencies, version, media);
wp_enqueue_script('handle', 'URL', dependencies, version, in_footer);
ArgumentRoleExample
HandleThe identifying name for this file (used to prevent duplicates)'main'
URLWhere the file isget_theme_file_uri('style.css')
DependenciesHandles of files that must load first['jquery']
VersionA string for cache busting'1.0.0'
Fifth argumentMedia for CSS ('all') / whether to put it in the footer for JStrue

The true in the fifth argument for JS matters. With false (the default) it loads in <head>, which delays when the page starts rendering. Default to true.

Functions that give you a file’s URL

get_theme_file_uri('style.css')          // a file in the theme (child theme takes priority)
get_stylesheet_uri()                     // the active theme's style.css
get_parent_theme_file_uri('style.css')   // a file in the parent theme

get_theme_file_uri() handles child themes too, which makes it safer than get_template_directory_uri() . '/style.css'.

TipsHow to build a child theme (minimal setup)How to load the parent’s CSS from a child theme

Version strings for cache busting

You fixed the CSS and nothing changed—the cause is no version string (or one that never changes).

// During development: use the file's modification time as the version automatically
$css = get_theme_file_path('style.css');
wp_enqueue_style('main', get_theme_file_uri('style.css'), [], filemtime($css));

filemtime() returns the file’s modification time, so the URL changes every time you save and a reload is guaranteed. After launch, bumping it by hand to something like '1.0.3' is fine too.

Using jQuery

jQuery ships with WordPress. Just naming it in the dependency array loads it.

wp_enqueue_script('main', get_theme_file_uri('js/main.js'), ['jquery'], '1.0.0', true);

The bundled jQuery runs in no-conflict mode, so $ isn’t available as-is. Wrap your code.

(function ($) {
  $('.menu-toggle').on('click', function () {
    $('.global-nav').toggleClass('is-open');
  });
})(jQuery);

Passing PHP values to JavaScript

Rather than hard-coding a <script> into a template, the proper route is to pass values as variables.

wp_enqueue_script('main', get_theme_file_uri('js/main.js'), [], '1.0.0', true);
wp_localize_script('main', 'MY_DATA', [
  'ajaxUrl' => admin_url('admin-ajax.php'),
  'homeUrl' => home_url('/'),
  'nonce'   => wp_create_nonce('my_action'),
]);
console.log(MY_DATA.homeUrl);

wp_localize_script() outputs the variable immediately before the script you named, so there’s nothing to worry about ordering-wise.

Loading different files on different pages

You don’t need to load everything on every page. Narrowing with conditionals makes things that much lighter.

function my_assets() {
  wp_enqueue_style('main', get_theme_file_uri('style.css'), [], '1.0.0');

  if (is_front_page()) {
    wp_enqueue_script('slider', get_theme_file_uri('js/slider.js'), [], '1.0.0', true);
  }
  if (is_singular('shohin')) {
    wp_enqueue_style('product', get_theme_file_uri('css/product.css'), ['main'], '1.0.0');
  }
}
add_action('wp_enqueue_scripts', 'my_assets');

Writing a dependency like ['main'] guarantees it always loads after main, so the override order is assured.

TipsWordPress conditional tags cheat sheetThe conditional tag cheat sheet

Loading in the admin screen and the block editor

HookTarget
wp_enqueue_scriptsThe site (front end)
admin_enqueue_scriptsThe admin screen
enqueue_block_editor_assetsThe block editor’s editing screen

When you want the post’s appearance reproduced inside the block editor too, load your CSS with enqueue_block_editor_assets.

Files registered with wp_enqueue_* are output at these two positions in your templates.

  <?php wp_head(); ?>
</head>
  <?php wp_footer(); ?>
</body>

Without those two lines, no CSS or JS is output at all (and plugins stop working too). It’s the very first thing to check when you’ve built a theme yourself.

Loading from an external CDN

wp_enqueue_style('lightbox', 'https://cdn.example.com/lightbox.min.css', [], null);

For external URLs, pass null as the version to keep the URL as-is. Bear in mind, though, that you’re exposed to the external service going down or changing—so downloading it and serving it yourself is safer in production.

Summary

  1. Load CSS and JS from functions.php with wp_enqueue_style() / wp_enqueue_script()
  2. Put JS in the footer with true as the fifth argument. Guarantee load order with the dependency array
  3. Bust caches with a version (filemtime() and friends). When changes don’t show, start here
  4. Pass PHP values to JS with wp_localize_script(). Without wp_head() / wp_footer(), nothing is output at all

Once you control loading, both your page speed and your trouble count change noticeably.

FAQ

Is it wrong to write a link tag directly in header.php?
It works, but avoid it. You lose control over load order and duplicates, you get compatibility problems with plugins, and you can't attach a version string for cache busting. Use wp_enqueue_style() and WordPress manages order and duplicates for you.
I edited my CSS but the browser doesn't change.
That's caching. Pass a version as the fourth argument of wp_enqueue_style and ?ver=… is appended to the URL, forcing a reload when it changes. During development, passing the file's modification time via filemtime() is handy.
How do I use jQuery in WordPress?
You usually don't need wp_enqueue_script('jquery')—just put ['jquery'] in the dependency array and it loads automatically. The bundled jQuery runs in no-conflict mode, so use jQuery instead of $, or wrap your code in function($){…}(jQuery).
I want to use a PHP value from JavaScript.
Pass it as a variable with wp_localize_script() (or wp_add_inline_script()). It's safer than hard-coding a script tag into a template, and the ordering is handled for you.