Default Header Images Generator
register_default_headers with the theme support it needs and the render callback it is useless without, plus the %s path placeholder done properly.
<?php
/**
* Default header images for the theme.
*
* These appear as choices under Appearance → Header. They are theme files, not uploads: the
* paths are relative to the theme directory, and register_default_headers() does nothing at all
* unless the theme also declares custom-header support.
*/
/**
* Declares header support and registers the choices.
*/
function my_theme_custom_header_setup() {
add_theme_support(
'custom-header',
array(
'default-image' => get_template_directory_uri() . '/assets/headers/mountains.jpg',
'width' => 1600,
'height' => 480,
// Without this the admin offers a cropper that will not let the user keep their own
// aspect ratio, which is the right trade only if the design truly requires the size.
'flex-width' => true,
'flex-height' => true,
'uploads' => true,
'header-text' => true,
'default-text-color' => 'ffffff',
'wp-head-callback' => 'my_theme_header_style',
)
);
// The %s in these paths is filled in by WordPress with the theme directory URI. Using a
// hardcoded URL here breaks the moment the site moves domain or the theme is renamed.
register_default_headers(
array(
'mountains' => array(
'url' => '%s/assets/headers/mountains.jpg',
'thumbnail_url' => '%s/assets/headers/mountains-thumb.jpg',
'description' => _x( 'Mountains at dawn', 'header image description', 'my-theme' ),
),
'harbour' => array(
'url' => '%s/assets/headers/harbour.jpg',
'thumbnail_url' => '%s/assets/headers/harbour-thumb.jpg',
'description' => _x( 'Harbour in winter', 'header image description', 'my-theme' ),
),
)
);
}
add_action( 'after_setup_theme', 'my_theme_custom_header_setup' );
/**
* Prints the header styles.
*
* Registered as wp-head-callback above. Without a callback the header image is registered and
* never rendered, which is the commonest reason "the image does not show up".
*/
function my_theme_header_style() {
$image = get_header_image();
if ( ! $image ) {
return;
}
?>
<style id="my_theme-header-css">
.site-header {
background-image: url( <?php echo esc_url( $image ); ?> );
background-position: center;
background-size: cover;
min-height: 480px;
}
</style>
<?php
}
Output is valid and updates as you type.
Fix the highlighted fields to update the output.
register_default_headers() on its own does nothing. It registers choices for a feature the theme has not
declared, so Appearance → Header does not appear and the images never show up. It needs
add_theme_support( 'custom-header' ), and it needs something to actually render the image, which is the
wp-head-callback.
Those two omissions are why most snippets for this do not work when pasted. All three parts are generated here together.
The other detail worth knowing: the url and thumbnail_url values take a literal %s, which WordPress
replaces with the theme directory URI. A hardcoded https://example.com/wp-content/themes/... works until
the site changes domain, and then every header image 404s.
How to use
- Put the images in a folder inside the theme and list them, with a thumbnail for each.
- Set the size, and decide whether the cropper forces it.
- Drop the file in the theme and require it from
functions.php.
Example
function my_theme_custom_header_setup() {
add_theme_support(
'custom-header',
array(
'default-image' => get_template_directory_uri() . '/assets/headers/mountains.jpg',
'width' => 1600,
'height' => 480,
'flex-width' => true,
'flex-height' => true,
'uploads' => true,
'header-text' => true,
'default-text-color' => 'ffffff',
'wp-head-callback' => 'my_theme_header_style',
)
);
register_default_headers(
array(
'mountains' => array(
'url' => '%s/assets/headers/mountains.jpg',
'thumbnail_url' => '%s/assets/headers/mountains-thumb.jpg',
'description' => _x( 'Mountains at dawn', 'header image description', 'my-theme' ),
),
)
);
}
add_action( 'after_setup_theme', 'my_theme_custom_header_setup' );
The callback that renders it is generated alongside, because without one the image is a registered choice that appears nowhere on the front end.
Pitfalls
These are theme files, not uploads. The images have to ship with the theme, in a folder inside it. A
child theme registering headers needs get_stylesheet_directory_uri() rather than the template one, and
%s resolves to the template directory, so a child theme’s own images need the full URI.
%s is not a typo. WordPress runs the registered URLs through sprintf with the theme directory URI.
Writing a full URL there survives until the domain changes; writing %s never breaks.
Turning off flex width and height forces the cropper. The user cannot then keep their own framing, and a photograph cropped to an exact 1600 × 480 usually loses the subject. Only do it when the design genuinely depends on the ratio.
Hiding header text is not the same as removing it. Setting header-text to false should hide the title
and tagline visually while leaving them in the markup for screen readers, which is what the generated CSS
does with clip-path. Using display: none removes them from the accessibility tree as well, and the site
title is usually the most useful thing on the page for a screen reader user.
Thumbnails are not optional in practice. Without thumbnail_url the admin screen loads the full-size
image for every choice, so a picker with eight 1600-pixel headers downloads eight megabytes.
Block themes handle this differently. A block theme puts the header in a template part with a cover
block, and custom-header support is mostly irrelevant there. This is for classic themes, and for a block
theme still carrying a classic header.
after_setup_theme is the hook. Registering on init is too late for theme support to be seen in
places that check it early, and registering at file load is too early for translation functions.
Compatibility
Everything runs in the browser: nothing is uploaded and nothing is stored.
register_default_headers() and custom-header support have been in WordPress since 3.4 and the argument
names have not changed. wp-head-callback defaults to _custom_header_h1_style, which does almost nothing
useful, so a real callback is generated here rather than left out.
The description goes through _x() with a context string, because a two-word image label is exactly the
kind of text a translator needs context for. Every value is escaped for a PHP single-quoted string: the
hostile fixture includes an apostrophe, */, ?> and ${x}, and the generated file passes php -l with
the same token structure as the default one.
The generated CSS uses background-size: cover and a min-height, which is the arrangement that survives
a flexible image size. A theme using an <img> in the header instead should render get_header_image_tag()
rather than this CSS, and that is a different callback.
Frequently asked questions
Why is Appearance → Header missing?
add_theme_support( 'custom-header' ) has not run, or has run too late. register_default_headers()
alone does not create the screen.Why does the image not appear on the front end?
wp-head-callback or a
get_header_image_tag() call in the template is what puts it on the page.What size should the header be?
Can the user upload their own?
uploads set to true, which is the default here. They then get the cropper, constrained by your
flex settings.Does this work in a child theme?
%s resolves to the parent theme directory. For images living in the child theme,
use get_stylesheet_directory_uri() and build the URLs yourself rather than relying on the placeholder.