14+ years building on WordPress / Replies in under 5 hours
WordPress 10 min read · Updated July 2026

How to Use wp_get_attachment_image() in WordPress Templates

Photo of Ajay Khandal
Ajay Khandal
WordPress Developer
TL;DR

wp_get_attachment_image() returns a full <img> element — with srcset, sizes, alt, and loading="lazy" already set — for any WordPress media library attachment. Pass the attachment ID (not the URL), the size name or array, and an optional attribute array for class/alt/loading overrides. Use get_post_thumbnail_id() for featured images, get_field('image') for ACF fields. The function returns an HTML string — you must echo it. For just the URL (background images, JavaScript), use wp_get_attachment_image_url() instead. Remove loading="false" from hero images: lazy-loading the LCP element hurts Core Web Vitals.

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 with add_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 (including class, alt, and loading).

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.

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-in attachment_url_to_postid()-style function in older versions — use attachment_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 no echo outputs 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' when add_image_size( 'hero', ... ) hasn’t been called in functions.php will 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.

Frequently asked questions

get_the_post_thumbnail() is a thin wrapper around wp_get_attachment_image(). It calls get_post_thumbnail_id() internally and passes the result to wp_get_attachment_image(). Use get_the_post_thumbnail() when you want the featured image and don't need the attachment ID separately. Use wp_get_attachment_image() directly when you have an attachment ID from another source — an ACF image field, a post meta value, a gallery of IDs — or when you want the featured image but also need to use the ID elsewhere in the template.

The function returns an empty string when the attachment ID is 0, false, or null; when the attachment doesn't exist in the database (deleted from media library); or when the $icon parameter is false (default) and the attachment is a non-image file type. Add a check before calling: if ( $attachment_id ) { echo wp_get_attachment_image(...); }. If you're sure the ID is valid and it still returns empty, confirm the attachment exists in wp-admin → Media and hasn't been deleted. The function does not throw — it silently returns ''.

Use wp_get_attachment_image_url( $attachment_id, $size ). It accepts the same first two parameters as wp_get_attachment_image() and returns just the URL string, or false if the attachment or size doesn't exist. This is the right function when you need the URL for a CSS background-image property, an Open Graph meta tag, or passing to JavaScript. If you also need the width and height, use wp_get_attachment_image_src() instead — it returns an array [url, width, height, is_intermediate].

Pass loading => false (or loading => 'eager') in the $attr array: wp_get_attachment_image( $id, 'full', false, [ 'loading' => false ] ). WordPress adds loading="lazy" to all images by default since version 5.5. That's correct for below-fold content, but the hero image is usually the Largest Contentful Paint element — lazy-loading it delays LCP and hurts your Core Web Vitals score. For the hero image specifically, also consider adding fetchpriority => 'high' to the $attr array to tell the browser to prioritise fetching it early in the page load sequence.

Any registered image size name: the built-in sizes are 'thumbnail' (150x150, hard-cropped), 'medium' (max 300px, proportional), 'medium_large' (768px wide), 'large' (max 1024px, proportional), and 'full' (original file). You can also pass a custom size registered with add_image_size(), or a two-element array like [800, 600] for a soft resize that uses the closest existing registered size. If you pass a size name that doesn't exist, WordPress falls back to the full-resolution original — no error, but potentially a very large file transfer. Check registered sizes with wp_get_registered_image_subsizes().

Yes. Set the ACF image field's return format to 'Image ID' in the field group settings. The field then returns an integer directly: $attachment_id = get_field('my_image'); echo wp_get_attachment_image( $attachment_id, 'large' );. If the return format is 'Image Object' (array), access the ID as $image['ID']. The Image ID format is simpler and faster when all you need is the rendered img element — use Image Object when you also need the caption, description, or multiple size URLs in the same template.

Photo of Ajay Khandal

Written by Ajay Khandal

I'm a freelance WordPress developer with 14+ years of experience building, fixing, and speeding up sites for businesses, agencies, and store owners across the US, UK, Europe, and Australia. I specialize in custom themes, WooCommerce, and performance — the kind of work that shows up as faster load times and fewer support tickets. No account managers, no outsourced tickets — you work directly with me, with replies typically inside 5 hours.

Work with me →