Loading Stylesheets and Scripts in WordPress
The hook name says scripts and it handles both. It also covers the front end only, which is why a stylesheet loaded here is missing from the block editor and from the login screen.
One hook, both asset types, front end only. wp_enqueue_scripts is where a theme or plugin registers the CSS and JavaScript a visitor loads. The naming misleads twice: it handles stylesheets as readily as scripts, and despite sounding general it never fires in the admin, the block editor or the login screen. Each of those has its own hook, and assets registered in the wrong one simply do not appear.
The shape
add_action( 'wp_enqueue_scripts', 'ti_assets' );
function ti_assets() {
$theme = wp_get_theme();
wp_enqueue_style(
'ti-main',
get_stylesheet_directory_uri() . '/assets/main.css',
array(),
$theme->get( 'Version' )
);
wp_enqueue_script(
'ti-behaviour',
get_stylesheet_directory_uri() . '/assets/behaviour.js',
array(),
$theme->get( 'Version' ),
true
);
}
The four and five arguments are all load-bearing. The handle must be unique across the whole site, because a collision means one file silently replaces another. The dependency array controls order. The version string controls caching. The final true on the script puts it in the footer, which is where most scripts belong.
The version argument is your cache busting
Browsers and CDNs cache assets by URL. If the URL never changes, the visitor keeps the file they already have, however many times you edit it. The version string is appended as a query argument and is what makes the URL differ.
Passing the theme version works only if you remember to bump it on every CSS change, which nobody does reliably. Using the file's modification time removes the discipline requirement:
$path = get_stylesheet_directory() . '/assets/main.css';
wp_enqueue_style(
'ti-main',
get_stylesheet_directory_uri() . '/assets/main.css',
array(),
file_exists( $path ) ? filemtime( $path ) : null
);
Note the two different functions. get_stylesheet_directory() returns a server path for filemtime; get_stylesheet_directory_uri() returns a URL for the browser. Passing a URL to filemtime produces a warning and no version, and the mix-up is easy to make because the names differ by four characters.
Passing null as the version omits the argument entirely, which lets WordPress fall back to its own version number — rarely what you want, since that changes only on core updates.
Parent and child themes
In a child theme, the two directory functions stop being interchangeable:
| Function | Points at |
|---|---|
get_stylesheet_directory_uri() |
The child theme |
get_template_directory_uri() |
The parent theme |
A child theme enqueuing its own assets wants the first. A child theme that needs the parent stylesheet loaded first names it as a dependency rather than enqueuing it by URL, so that order is guaranteed rather than incidental. The setup is covered in creating a WordPress child theme.
Where else assets have to be registered
- The block editor loads through
enqueue_block_editor_assets. A theme whose front end styles are missing in the editor gives authors a preview that does not match the published page, which is the usual complaint behind the block editor not working as expected. - The admin uses
admin_enqueue_scripts, which receives the current screen hook so assets can be limited to one page instead of loading everywhere. - The login screen uses
login_enqueue_scripts.
Loading everything everywhere is the lazy fix and it has a cost: admin assets slow every dashboard page, and that shows up as a slow WordPress admin.
Conditional loading
A script needed on one template should not load on all of them. Conditional tags work inside the callback because it runs late enough for the query to be resolved:
if ( is_singular( 'post' ) ) {
wp_enqueue_script( 'ti-share', /* ... */ );
}
This is the cheapest performance work available on most themes, and it directly reduces the render-blocking asset count described in fixing render-blocking resources.
Verify with a hard reload and view source
Load a page, view source, and confirm the handle and version appear on the URL. Then edit the file, reload normally, and confirm the version changed. If it did not, cache busting is not working and every future change will reach visitors late.
Check the block editor and one admin screen too. Assets that appear on the front end and nowhere else are correct for this hook — the absence is the expected behaviour, not a bug, and adding custom CSS through a different route is what covers the gap.
Frequently asked
- Because this hook is front end only. The editor loads its assets through enqueue_block_editor_assets, the admin through admin_enqueue_scripts and the login page through login_enqueue_scripts. Each is a separate registration.
- Because the version argument did not change. Browsers cache by URL, and the version string is what makes the URL differ. Passing the theme version means the URL only changes when you remember to bump it.
- Yes, and it is the only reliable way to control it. Naming a handle as a dependency guarantees that handle is output first, which matters for anything extending a library rather than merely coexisting with it.