Display a WooCommerce product gallery with PHP in any template

WooCommerce product galleries — the thumbnail strip below the main product image that lets shoppers view additional photos — are one of the highest-impact elements on a product page. By default, WooCommerce renders the gallery using its own template and Flexslider/Photoswipe JavaScript. But sometimes you need to output the gallery in a custom template, a shortcode, a widget, or an AJAX response where the default template output is not available. WooCommerce stores gallery image IDs in the _product_image_gallery post-meta key as a comma-separated list of attachment IDs. You retrieve the main image with get_post_thumbnail_id() and the gallery images with $product->get_gallery_image_ids(). From those IDs you can generate whatever HTML structure you need using standard WordPress attachment functions: wp_get_attachment_image(), wp_get_attachment_image_src(), and wp_get_attachment_image_url(). For a lightbox effect, you can leverage the built-in WooCommerce Photoswipe integration or add your own data attributes for a custom lightbox library. This pattern is especially useful when combined with the product price display guide and shortcode patterns to build custom product showcase components. The example below outputs a fully functional gallery with the main image, thumbnail strip, and Photoswipe-compatible data attributes.

Problem: You need to display a WooCommerce product’s main image and gallery thumbnails in a custom template or shortcode outside of the standard single-product template.

Solution: Add the following code to your functions.php or template file:

/**
 * Output a WooCommerce product gallery for any product by ID.
 *
 * @param int $product_id
 * @return string HTML output
 */
function helloadmin_product_gallery( int $product_id ) {
    $product = wc_get_product( $product_id );
    if ( ! $product ) {
        return '';
    }

    $main_id    = $product->get_image_id();
    $gallery_ids = $product->get_gallery_image_ids();
    $all_ids    = $main_id ? array_merge( [ $main_id ], $gallery_ids ) : $gallery_ids;

    if ( empty( $all_ids ) ) {
        return wc_placeholder_img( 'woocommerce_single' );
    }

    ob_start();
    echo '<div class="helloadmin-product-gallery">';

    // Main image (first in array)
    $main_src = wp_get_attachment_image_src( $all_ids[0], 'woocommerce_single' );
    if ( $main_src ) {
        printf(
            '<a href="%s" class="gallery-main" data-photoswipe-src="%s">%s</a>',
            esc_url( $main_src[0] ),
            esc_url( wp_get_attachment_url( $all_ids[0] ) ),
            wp_get_attachment_image( $all_ids[0], 'woocommerce_single', false, [
                'class' => 'gallery-main-img',
                'alt'   => esc_attr( $product->get_name() ),
            ] )
        );
    }

    // Thumbnail strip
    if ( count( $all_ids ) > 1 ) {
        echo '<div class="gallery-thumbs">';
        foreach ( $all_ids as $attachment_id ) {
            $thumb_src = wp_get_attachment_image_src( $attachment_id, 'thumbnail' );
            if ( $thumb_src ) {
                printf(
                    '<a href="%s" class="gallery-thumb" data-full="%s">%s</a>',
                    esc_url( $thumb_src[0] ),
                    esc_url( wp_get_attachment_url( $attachment_id ) ),
                    wp_get_attachment_image( $attachment_id, 'thumbnail', false, [
                        'class' => 'gallery-thumb-img',
                        'alt'   => esc_attr( get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ) ),
                    ] )
                );
            }
        }
        echo '</div>';
    }

    echo '</div>';
    return ob_get_clean();
}

// Use as a shortcode: [helloadmin_gallery id="42"]
add_shortcode( 'helloadmin_gallery', function( $atts ) {
    $atts = shortcode_atts( [ 'id' => 0 ], $atts );
    return helloadmin_product_gallery( absint( $atts['id'] ) );
} );

NOTE: wc_placeholder_img() returns WooCommerce’s default grey placeholder image when no product image is set — always include this fallback to avoid empty image elements on products without photos. If you need a responsive gallery with swipe support on mobile, consider adding data- attributes compatible with Swiper.js or Splide (both lightweight alternatives to the default Flexslider) rather than building swipe handling from scratch.