wp_get_attachment_image() returns a complete <img> element for any attachment stored in the WordPress media library. Unlike hardcoding an image URL, it generates the correct srcset and sizes attributes automatically, using every registered image size WordPress has generated for that file. It handles the alt attribute from the media library entry, adds loading="lazy" by default (since WordPress 5.5), and produces valid, accessible markup in a single function call.
You’ll reach for it whenever you’re building a custom theme template and need to display an image — a featured image, an ACF image field, a gallery attachment — without building the <img> tag by hand. For context on when to use custom templates versus a page builder, see page builders vs. custom code for WordPress.
Function Signature
wp_get_attachment_image(
int $attachment_id,
string|array $size = 'thumbnail',
bool $icon = false,
string|array $attr = ''
)
The function returns an HTML string — it does not echo. You must echo or print the return value to output anything.
$attachment_id(int, required)- The ID of the attachment post — not the URL, not the filename. See the section below on how to get the right ID.
$size(string|array, default: ‘thumbnail’)- A registered image size name (
'thumbnail','medium','large','full', or a custom size registered withadd_image_size()), or a two-element array[width, height]for a soft-resized output. $icon(bool, default: false)- When
true, returns a mime-type icon for non-image attachments (PDFs, audio files, etc.) rather than an actual image. Rarely used in practice. $attr(string|array, default: ”)- Extra HTML attributes to add to the
<img>tag, as an associative array. Values in this array override any defaults WordPress sets (includingclass,alt, andloading).
Getting the Attachment ID
The most common mistake with this function is not knowing where to get the attachment ID. These are the three most common sources.
Featured Image
$attachment_id = get_post_thumbnail_id( get_the_ID() );
if ( $attachment_id ) {
echo wp_get_attachment_image( $attachment_id, 'large' );
}
get_post_thumbnail_id() returns an integer when a featured image is set, or false when it isn’t — check before passing it to wp_get_attachment_image(). Inside the loop you can omit the argument: get_post_thumbnail_id() uses the current post.
ACF Image Field
ACF image fields have two return formats set in the field configuration.
// Return format: Image Object (array)
$image = get_field( 'hero_image' );
if ( $image ) {
echo wp_get_attachment_image( $image['ID'], 'large' );
}
// Return format: Image ID (integer — cleaner for this use case)
$attachment_id = get_field( 'hero_image' );
if ( $attachment_id ) {
echo wp_get_attachment_image( $attachment_id, 'large' );
}
The “Image ID” return format is simpler when all you’re doing is calling wp_get_attachment_image() — the array format is useful when you also need the URL or caption alongside.
Custom Post Meta
$attachment_id = (int) get_post_meta( get_the_ID(), 'my_image_id', true );
if ( $attachment_id ) {
echo wp_get_attachment_image( $attachment_id, 'large' );
}
Post meta is stored as strings, so cast to int before passing.
The $size Parameter
Built-in WordPress Image Sizes
| Size name | Default dimensions | Crop |
|---|---|---|
'thumbnail' |
150 × 150 px | Hard crop (square) |
'medium' |
max 300 × 300 px | Soft resize (proportional) |
'medium_large' |
768 px wide | Soft resize, no height limit |
'large' |
max 1024 × 1024 px | Soft resize (proportional) |
'full' |
Original file dimensions | No resize |
The admin can change thumbnail/medium/large dimensions in Settings → Media. Changing them does not regenerate existing images — only newly uploaded files get the new sizes. Use a plugin like Regenerate Thumbnails or WP-CLI (wp media regenerate --all) to backfill.
Custom Registered Sizes
// In functions.php — register a custom size
add_image_size( 'hero', 1200, 500, true ); // 1200×500, hard cropped
Once registered, pass the size name as a string:
echo wp_get_attachment_image( $attachment_id, 'hero' );
If the file was uploaded before the size was registered, it won’t exist yet — WordPress will fall back to the closest larger registered size. Regenerate thumbnails after adding a new size.
Array Format for Soft Resize
// Width 800, unconstrained height
echo wp_get_attachment_image( $attachment_id, [ 800, 0 ] );
// Both dimensions constrained — largest fit, no crop
echo wp_get_attachment_image( $attachment_id, [ 800, 600 ] );
Passing an array does not generate a new file — WordPress picks the closest registered size that’s at least as large as the requested dimensions and lets the browser handle the visual resize. For pixel-perfect control, register a named size with add_image_size() instead.
The $attr Parameter
Pass an associative array of HTML attributes. These override WordPress’s defaults.
Adding a CSS Class
echo wp_get_attachment_image(
$attachment_id,
'large',
false,
[ 'class' => 'hero-image wp-post-image' ]
);
WordPress normally adds class="attachment-{size} size-{size}". Passing 'class' in $attr replaces that entirely — include both defaults and your own class names if you need them.
Overriding Alt Text
echo wp_get_attachment_image(
$attachment_id,
'large',
false,
[ 'alt' => esc_attr( get_the_title() ) ]
);
WordPress uses the alt text set on the media library item by default. Override it when the context requires something different — for example, an image used as a decorative separator (pass an empty string for 'alt') or a product image where the alt should include the variant name rather than the upload description.
Adding a Title Attribute
echo wp_get_attachment_image(
$attachment_id,
'large',
false,
[ 'title' => esc_attr( $caption ) ]
);
WordPress does not add a title attribute by default. You can add one via $attr when you have a reason to — tooltips on interactive image galleries, for instance. If you’re using Elementor rather than custom PHP and need to set a title on an image widget, see how to add a title tag to an image in Elementor.
Controlling Lazy Loading
// Default: WordPress adds loading="lazy" automatically
// Remove it for the LCP hero image (above the fold):
echo wp_get_attachment_image(
$attachment_id,
'full',
false,
[ 'loading' => false ]
);
// Or force eager loading explicitly:
echo wp_get_attachment_image(
$attachment_id,
'full',
false,
[ 'loading' => 'eager' ]
);
WordPress adds loading="lazy" to all images by default since version 5.5. That’s correct behaviour for below-fold images, but the hero image or featured image at the top of the page is likely the Largest Contentful Paint element. Lazy-loading the LCP image delays it — pass 'loading' => false or 'loading' => 'eager' for above-fold hero images to avoid penalising your Core Web Vitals score.
Related Image Functions
get_the_post_thumbnail()
A thin wrapper around wp_get_attachment_image() that calls get_post_thumbnail_id() internally. Use it when you only need the featured image and want to skip the ID lookup step.
// Inside the loop:
echo get_the_post_thumbnail( get_the_ID(), 'large', [ 'class' => 'featured-img' ] );
// With has_post_thumbnail() guard:
if ( has_post_thumbnail() ) {
echo get_the_post_thumbnail( null, 'large' ); // null = current post
}
wp_get_attachment_image_url()
Returns a URL string, not an <img> element. Use this when you need the image as a CSS background-image value or in JavaScript.
$url = wp_get_attachment_image_url( $attachment_id, 'large' );
if ( $url ) {
echo '<div style="background-image: url(' . esc_url( $url ) . ')"></div>';
}
wp_get_attachment_image_src()
The older, lower-level version. Returns an array [url, width, height, is_intermediate], or false if the attachment or size doesn’t exist. Useful when you need dimensions alongside the URL.
$src = wp_get_attachment_image_src( $attachment_id, 'large' );
if ( $src ) {
[$url, $width, $height] = $src;
echo '<img src="' . esc_url( $url ) . '" width="' . $width . '" height="' . $height . '" alt="">';
}
For most cases, wp_get_attachment_image() is the right choice — it handles srcset, sizes, lazy-loading, and accessibility attributes automatically. Use wp_get_attachment_image_url() or _src() only when you genuinely need the URL rather than the full element.
Template Examples
Featured Image in a Custom Post Loop
if ( have_posts() ) :
while ( have_posts() ) :
the_post();
?>
<article id="post-<?php the_ID(); ?>">
<?php if ( has_post_thumbnail() ) : ?>
<a href="<?php the_permalink(); ?>">
<?php
echo wp_get_attachment_image(
get_post_thumbnail_id(),
'medium',
false,
[ 'class' => 'post-thumbnail', 'alt' => esc_attr( get_the_title() ) ]
);
?>
</a>
<?php endif; ?>
<h2><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h2>
</article>
<?php
endwhile;
endif;
ACF Gallery Field Loop
$gallery = get_field( 'product_gallery' ); // return format: array of IDs
if ( $gallery ) :
echo '<ul class="product-gallery">';
foreach ( $gallery as $image_id ) {
echo '<li>';
echo wp_get_attachment_image( $image_id, 'large', false, [ 'loading' => 'lazy' ] );
echo '</li>';
}
echo '</ul>';
endif;
Hero Image Without Lazy Loading
$hero_id = get_field( 'hero_image' ); // return format: ID
if ( $hero_id ) {
echo wp_get_attachment_image(
$hero_id,
'full',
false,
[
'class' => 'hero-image',
'loading' => false, // no lazy-load — this is the LCP element
'fetchpriority' => 'high', // browser hint to prioritise this resource
]
);
}
For custom theme templates, getting the post title in WordPress templates covers the other common template tag pair (get_the_title() / the_title()) you’ll use alongside image output.
Responsive Images and srcset
One of the main reasons to use wp_get_attachment_image() over a hardcoded <img> tag is that WordPress builds the srcset and sizes attributes automatically based on the sizes registered in your theme. The output for a 'large' size call on a 2000px-wide upload might look like:
<img
src="https://example.com/wp-content/uploads/2026/01/photo-1024x683.jpg"
class="attachment-large size-large"
alt="Alt text from media library"
width="1024"
height="683"
srcset="https://example.com/wp-content/uploads/2026/01/photo-300x200.jpg 300w,
https://example.com/wp-content/uploads/2026/01/photo-768x512.jpg 768w,
https://example.com/wp-content/uploads/2026/01/photo-1024x683.jpg 1024w,
https://example.com/wp-content/uploads/2026/01/photo-2000x1333.jpg 2000w"
sizes="(max-width: 1024px) 100vw, 1024px"
loading="lazy"
/>
The browser picks the most appropriate file from srcset based on the viewport width and device pixel ratio. High-DPI (retina) mobile screens get a higher-resolution variant automatically, without any extra code on your part.
Common Mistakes
- Passing a URL instead of an ID.
wp_get_attachment_image()takes an integer attachment ID. If you have a URL, WordPress does not have a built-inattachment_url_to_postid()-style function in older versions — useattachment_url_to_postid( $url )(added in WP 4.0) if you must convert, but it’s a database query, so store IDs where possible rather than doing the reverse lookup at render time. - Not echoing the return value. The function returns an HTML string. Calling
wp_get_attachment_image( $id, 'large' )with noechooutputs nothing — there is no output buffer magic here. This is the number-one reason developers see a blank space where an image should be. - Using a size name that isn’t registered. Passing
'hero'whenadd_image_size( 'hero', ... )hasn’t been called infunctions.phpwill fall back to the original file size. The output won’t error — you’ll just get the full-resolution original, which is usually not what you want. - Assuming the attachment exists.
wp_get_attachment_image()returns an empty string if the attachment ID is 0, false, or doesn’t exist in the database. Always check that the ID is truthy before calling, or check the return value before echoing. - Using
'full'size for frontend display.'full'returns the original uploaded file — potentially a 6000×4000 JPEG from a DSLR. The browser scales it down, but the full file still transfers. Use a registered named size that matches your layout’s actual maximum display width.


