The cleanest pattern for responsive images from an ACF Image field is to use the Image ID return format plus wp_get_attachment_image(). WordPress auto-generates a srcset attribute from your registered image sizes and a sensible sizes default, and you write one line of template code. The manual srcset pattern is the fallback when you need explicit control. The <picture> element with multiple <source> tags handles the art-direction case. Here is each in detail.
Jump to:
- Step 1: Register the image sizes you actually want
- Step 2: Set the ACF field to return Image ID
- Step 3: Use wp_get_attachment_image for the one-liner pattern
- The manual srcset pattern
- The picture element for art direction
- Performance defaults that matter
- Cloudflare image transforms as a delivery layer
Step 1: Register the image sizes you actually want
WordPress generates resized versions of every uploaded image based on the sizes registered in your theme. The default sizes (thumbnail, medium, large, full) are fine for most cases but you almost always want a few custom sizes tuned to your layout breakpoints:
// In functions.php
add_action( 'after_setup_theme', function () {
add_image_size( 'hero-mobile', 768, 432, true ); // 16:9 cropped
add_image_size( 'hero-tablet', 1280, 720, true );
add_image_size( 'hero-desktop', 1920, 1080, true );
add_image_size( 'card-thumb', 480, 320, true );
} );The third argument (true) hard-crops to the exact dimensions. Without it, the image is scaled to fit within the bounding box without cropping.
The full reference is in add_image_size. Note: registering new sizes only generates them for images uploaded AFTER the registration. To backfill existing images, run wp media regenerate --image_size=hero-desktop or use a plugin like Regenerate Thumbnails.
Step 2: Set the ACF field to return Image ID
In the field group editor, set the field's Return Format to "Image ID". This stores the attachment ID and lets the helpers below work cleanly. The trade-offs of this vs Image Array are covered in ACF Image Field Returning an Array Instead of a URL?.
Step 3: Use wp_get_attachment_image for the one-liner pattern
$hero_id = get_field( 'hero_image' );
if ( $hero_id ) {
echo wp_get_attachment_image( $hero_id, 'hero-desktop', false, [
'class' => 'hero-image w-full h-auto',
'loading' => 'eager', // hero is above-the-fold
'fetchpriority' => 'high', // tell the browser to prioritize
'sizes' => '(min-width: 1280px) 1280px, 100vw',
] );
}wp_get_attachment_image() produces a complete <img> tag including:
src(the requested size variant)widthandheightattributes (which prevent CLS)alt(pulled from the attachment's alt meta automatically)srcset(auto-generated from all registered sizes that exist for this image)sizes(the value you passed, or a default of(max-width: <width>px) 100vw, <width>px)- Any additional attributes you pass in the fourth argument
The output looks roughly like:
<img width="1920" height="1080" src=".../hero-1920x1080.jpg"
class="hero-image w-full h-auto"
loading="eager" fetchpriority="high"
alt="Sunset over a city skyline"
srcset=".../hero-768x432.jpg 768w, .../hero-1280x720.jpg 1280w, .../hero-1920x1080.jpg 1920w"
sizes="(min-width: 1280px) 1280px, 100vw">The browser picks the best srcset variant for the device's DPR and viewport width. The sizes attribute tells the browser how much space the image will take in the layout.
The manual srcset pattern
When you want full control (custom srcset, different image variants per breakpoint, conditional logic), build the srcset manually:
$hero_id = get_field( 'hero_image' );
if ( $hero_id ) {
$alt = get_post_meta( $hero_id, '_wp_attachment_image_alt', true );
$src_desktop = wp_get_attachment_image_src( $hero_id, 'hero-desktop' );
// wp_get_attachment_image_src returns [url, width, height, is_intermediate]
$url = $src_desktop[0];
$width = $src_desktop[1];
$height = $src_desktop[2];
$srcset = wp_get_attachment_image_srcset( $hero_id, 'hero-desktop' );
$sizes = '(min-width: 1280px) 1280px, 100vw';
printf(
'<img src="%s" srcset="%s" sizes="%s" width="%d" height="%d" alt="%s" loading="eager">',
esc_url( $url ),
esc_attr( $srcset ),
esc_attr( $sizes ),
intval( $width ),
intval( $height ),
esc_attr( $alt )
);
}Use this when the auto-generated srcset from wp_get_attachment_image() includes sizes you do not want, or when you need conditional srcset logic.
The picture element for art direction
When you want fundamentally different images per breakpoint (a vertical crop for mobile, a wide crop for desktop), the <picture> element with multiple <source> tags is the right tool:
$mobile_id = get_field( 'hero_image_mobile' );
$desktop_id = get_field( 'hero_image_desktop' );
if ( $mobile_id && $desktop_id ) {
$alt = get_post_meta( $desktop_id, '_wp_attachment_image_alt', true );
$mobile = wp_get_attachment_image_src( $mobile_id, 'hero-mobile' );
$desktop = wp_get_attachment_image_src( $desktop_id, 'hero-desktop' );
?>
<picture>
<source media="(max-width: 767px)" srcset="<?php echo esc_url( $mobile[0] ); ?>">
<img src="<?php echo esc_url( $desktop[0] ); ?>"
width="<?php echo intval( $desktop[1] ); ?>"
height="<?php echo intval( $desktop[2] ); ?>"
alt="<?php echo esc_attr( $alt ); ?>"
loading="eager">
</picture>
<?php
}This pattern uses two separate ACF fields (one per crop), which is the right approach when the editor needs to choose different images for different breakpoints, not just different sizes of the same image.
Performance defaults that matter
A few defaults to bake into every responsive image you ship:
- Always set
widthandheighton the<img>. This prevents Cumulative Layout Shift (CLS).wp_get_attachment_image()does this automatically; manual patterns must do it explicitly. loading="eager"andfetchpriority="high"for above-the-fold images. The hero image is the LCP element on most pages; tell the browser to prioritize it.loading="lazy"for below-the-fold images. The default in modern WordPress, but verify if you are doing manual<img>output.- Sensible
sizesattribute. The default(max-width: <width>px) 100vw, <width>pxis fine for most cases; for fixed-width layouts, set it explicitly (sizes="600px"for a 600px fixed column). - JPEG/PNG source with WebP/AVIF served at request time. Modern hosting (or Cloudflare's image transform proxy) serves WebP/AVIF to supporting browsers without you maintaining alternate formats.
Cloudflare image transforms as a delivery layer
If you serve images through Cloudflare, the cdn-cgi/image transform proxy is a powerful delivery-time optimization that complements the WordPress-level srcset. You author one source size (the original); Cloudflare serves the right size, format, and quality per request:
<img src="https://your-cdn.com/cdn-cgi/image/width=1280,format=auto,quality=85/path/to/image.jpg"
alt="..." width="1280" height="720">Combined with srcset:
<img srcset="
https://your-cdn.com/cdn-cgi/image/width=768,format=auto/path/to/image.jpg 768w,
https://your-cdn.com/cdn-cgi/image/width=1280,format=auto/path/to/image.jpg 1280w,
https://your-cdn.com/cdn-cgi/image/width=1920,format=auto/path/to/image.jpg 1920w
" sizes="(min-width: 1280px) 1280px, 100vw">This is the pattern I use on TechEarl itself. The R2-backed images are stored once as JPEG; Cloudflare's transform proxy serves WebP/AVIF in the right size to every browser at request time. For most agency client sites, the simpler wp_get_attachment_image() pattern is sufficient and does not require a Cloudflare dependency.
For the broader agency-image-stack reasoning, see The Exact Stack I'd Use to Run a Small WordPress Agency Today. For the alt-text side of image fields, see How to Get ALT Text from an ACF Image Field.
Sources
Authoritative references this article was fact-checked against.
- wp_get_attachment_image() function reference (WordPress Developer Resources)developer.wordpress.org
- wp_get_attachment_image_srcset() function reference (WordPress Developer Resources)developer.wordpress.org
- add_image_size() function reference (WordPress Developer Resources)developer.wordpress.org





