Getting the alt text from an ACF Image field depends on which Return Format the field is configured to use. Image Array gives you the alt as a property of the returned array. Image ID and Image URL formats require you to fetch the alt separately via WordPress's standard attachment meta. After a thousand "why is my alt text empty" debugging sessions, here is the canonical pattern for each return format plus the always-correct version that works regardless of how the field is configured.
Jump to:
- The three Return Formats refresher
- Return Format: Image Array
- Return Format: Image ID
- Return Format: Image URL
- The always-correct pattern
- Where alt text lives in the database
- Fallback chain when alt is empty
The three Return Formats refresher
ACF Image fields can return three different shapes depending on the Return Format setting in the field group:
- Image Array (default in modern ACF Pro): the full array of attachment data including the alt text.
- Image ID (integer): just the attachment ID. You query the alt separately.
- Image URL (string): just the URL of the original. Getting the alt requires looking up the attachment by URL (slow) or by ID stored elsewhere.
For deeper coverage of the trade-offs between these formats, see ACF Image Field Returning an Array Instead of a URL?.
Return Format: Image Array
The simplest case:
$image = get_field( 'hero_image' );
if ( $image ) {
$alt = $image['alt']; // direct property of the returned array
$url = $image['url'];
printf( '<img src="%s" alt="%s">', esc_url( $url ), esc_attr( $alt ) );
}$image['alt'] is the alt text stored on the attachment in WordPress's media library. If the editor left the alt blank in the media library, this value is an empty string.
The Image Array also contains alt text per registered size if you need it: $image['sizes'] is the size-name keyed array of URLs, but the alt text is on the parent array (alt text is a property of the attachment, not of individual size variants).
Return Format: Image ID
When the field stores just the attachment ID:
$image_id = get_field( 'hero_image' );
if ( $image_id ) {
$alt = get_post_meta( $image_id, '_wp_attachment_image_alt', true );
$url = wp_get_attachment_image_url( $image_id, 'large' );
printf( '<img src="%s" alt="%s">', esc_url( $url ), esc_attr( $alt ) );
}The alt text is stored in wp_postmeta under the meta key _wp_attachment_image_alt. WordPress provides no top-level helper function for "give me an attachment's alt"; you read the meta directly.
Return Format: Image URL
The Image URL return format is the most awkward for alt text because you only get the URL, not the ID. To get the alt you have to look up the attachment by URL first:
$image_url = get_field( 'hero_image' );
if ( $image_url ) {
$image_id = attachment_url_to_postid( $image_url );
$alt = $image_id ? get_post_meta( $image_id, '_wp_attachment_image_alt', true ) : '';
printf( '<img src="%s" alt="%s">', esc_url( $image_url ), esc_attr( $alt ) );
}This is one extra database query per image and is the reason I almost never use the Image URL return format on production sites that need alt text. If you can switch to Image Array or Image ID, do.
The always-correct pattern
If you have a mixed codebase where different field instances use different return formats (very common on long-lived sites), this helper handles all three cases:
function get_acf_image_alt( $field_name, $post_id = false ) {
$value = get_field( $field_name, $post_id );
if ( empty( $value ) ) return '';
// Image Array
if ( is_array( $value ) && isset( $value['alt'] ) ) {
return $value['alt'];
}
// Image ID
if ( is_numeric( $value ) ) {
return get_post_meta( (int) $value, '_wp_attachment_image_alt', true );
}
// Image URL
if ( is_string( $value ) ) {
$id = attachment_url_to_postid( $value );
return $id ? get_post_meta( $id, '_wp_attachment_image_alt', true ) : '';
}
return '';
}Drop that into a mu-plugin or theme functions.php and call get_acf_image_alt( 'hero_image', $post_id ) regardless of the field's return format.
Where alt text lives in the database
For anyone debugging this from raw SQL or wondering where the value actually lives:
SELECT meta_value
FROM wp_postmeta
WHERE post_id = 42 -- the attachment ID
AND meta_key = '_wp_attachment_image_alt';The _wp_attachment_image_alt meta key is owned by WordPress core, not ACF. ACF stores the attachment ID (or URL, depending on return format), and the alt is on the attachment itself.
This matters because if you update the alt text via the media library, every place across the site that reads that attachment's alt picks up the new value automatically. There is no per-field alt; the alt belongs to the image, not to the place it is used.
Fallback chain when alt is empty
For SEO and accessibility, you usually do not want to ship an <img> with an empty alt attribute. A reasonable fallback chain:
function safe_image_alt( $image_id, $post_id = null ) {
$alt = get_post_meta( $image_id, '_wp_attachment_image_alt', true );
if ( ! empty( $alt ) ) return $alt;
// Fallback 1: image caption
$attachment = get_post( $image_id );
if ( ! empty( $attachment->post_excerpt ) ) {
return $attachment->post_excerpt;
}
// Fallback 2: image title
if ( ! empty( $attachment->post_title ) ) {
return $attachment->post_title;
}
// Fallback 3: post title where the image is used
if ( $post_id ) {
return get_the_title( $post_id );
}
return '';
}The order matters: editor-provided alt is best, then caption (which the editor likely wrote with the image), then title (often filename-derived and noisy), then the host post title as a last resort.
For agency-scale audits of missing alt text across a site, the WP-CLI patterns in Using AI with WP-CLI for Faster WordPress Operations work well: enumerate every attachment, check alt presence, generate a report of missing-alt images for the editor to fill in.
Sources
Authoritative references this article was fact-checked against.
- Image field (Advanced Custom Fields docs)advancedcustomfields.com
- get_post_meta() function reference (WordPress Developer Resources)developer.wordpress.org





