TechEarl

Proper Responsive Images with ACF Image Fields

The cleanest pattern for responsive images from an ACF Image field: ID return format plus wp_get_attachment_image, which produces a complete srcset and sizes attribute from registered image sizes. Plus the manual srcset pattern when you need control.

Ishan Karunaratne⏱️ 5 min readUpdated
Share thisCopied
Clean responsive images from ACF Image fields. ID + wp_get_attachment_image for auto srcset, manual srcset control, picture element for art direction.

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

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:

php
// 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

php
$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)
  • width and height attributes (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:

html
<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:

php
$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:

php
$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 width and height on the <img>. This prevents Cumulative Layout Shift (CLS). wp_get_attachment_image() does this automatically; manual patterns must do it explicitly.
  • loading="eager" and fetchpriority="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 sizes attribute. The default (max-width: <width>px) 100vw, <width>px is 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:

html
<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:

html
<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.

TagsWordPressACFImagesPerformanceResponsive

Found this useful? Pass it on.

Copied

Ishan Karunaratne

Tech Architect · Software Engineer · AI/DevOps

Tech architect and software engineer with 20+ years building software, Linux systems, and DevOps infrastructure, and lately working AI into the stack. Currently Chief Technology Officer at a healthcare tech startup, which is where most of these field notes come from.

Keep reading

Related posts

How to Get ALT Text from an ACF Image Field

Getting alt text from an ACF Image field depends on the field's Return Format. Image Array gives you the alt directly. ID and URL formats need a wp_get_attachment helper. Plus the cleanest pattern for always-correct alt output.

How to Set Default Values in ACF Select Fields

ACF Select fields have a Default Value setting in the field group editor that handles the simple case. For dynamic defaults (computed from another field, role-based, or per-post-type), the acf/load_value filter is the right tool.

Sending Gravity Forms Data Into ACF Repeater Fields

Gravity Forms does not natively submit Repeater-shaped data, but three patterns handle the common cases: append-per-submission, single submission with grouped sections, and a custom JSON-encoded field. Here is each with code.