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().
What the arguments mean
wp_enqueue_style('handle', 'URL', dependencies, version, media);
wp_enqueue_script('handle', 'URL', dependencies, version, in_footer);| Argument | Role | Example |
|---|---|---|
| Handle | The identifying name for this file (used to prevent duplicates) | 'main' |
| URL | Where the file is | get_theme_file_uri('style.css') |
| Dependencies | Handles of files that must load first | ['jquery'] |
| Version | A string for cache busting | '1.0.0' |
| Fifth argument | Media for CSS ('all') / whether to put it in the footer for JS | true |
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 themeget_theme_file_uri() handles child themes too, which makes it safer than get_template_directory_uri() . '/style.css'.
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.
Loading in the admin screen and the block editor
| Hook | Target |
|---|---|
wp_enqueue_scripts | The site (front end) |
admin_enqueue_scripts | The admin screen |
enqueue_block_editor_assets | The 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.
wp_head() and wp_footer() are required
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
- Load CSS and JS from
functions.phpwithwp_enqueue_style()/wp_enqueue_script() - Put JS in the footer with
trueas the fifth argument. Guarantee load order with the dependency array - Bust caches with a version (
filemtime()and friends). When changes don’t show, start here - Pass PHP values to JS with
wp_localize_script(). Withoutwp_head()/wp_footer(), nothing is output at all
Once you control loading, both your page speed and your trouble count change noticeably.