How to Make a Custom Product Loop in WooCommerce
WooCommerce is an open source e-commerce platform built on WordPress that powers online shopping for a huge share of WordPress sites. At some point almost every store needs a custom WooCommerce product loop — the default archive markup rarely matches a specific design, and you’ll want your own classes and structure to style it exactly the way you’ve imagined.
What Is the Product Loop?
The product loop is the collection of products displayed on a shop, category, or archive page. A custom product loop lets you control that markup directly — useful for a featured-products shortcode, a custom homepage section, or anywhere the default WooCommerce archive template doesn’t fit.
In this guide, we’ll build a working shortcode-based product loop, with full access to your own CSS classes, and fix a few real bugs found in a common version of this snippet along the way.
The Shortcode
Copy the code below into your theme’s functions.php file, or better, a small site-specific plugin.
<?php
add_shortcode( 'custom_loop', 'wpwebguru_custom_product_loop' );
function wpwebguru_custom_product_loop() {
ob_start();
$meta_query = WC()->query->get_meta_query();
$meta_query[] = array(
'key' => '_featured',
'value' => 'yes',
);
$args = array(
'post_type' => 'product',
'post_status' => 'publish',
'posts_per_page' => 6,
'orderby' => 'date',
'order' => 'DESC',
'meta_query' => $meta_query, // Use it if you want featured products only
'tax_query' => array(
array(
'taxonomy' => 'product_visibility',
'field' => 'name',
'terms' => 'outofstock',
'operator' => 'NOT IN',
),
),
);
$loop = new WP_Query( $args );
?>
<ul class="custom-product-loop">
<?php
while ( $loop->have_posts() ) :
$loop->the_post();
global $product;
?>
<li>
<?php
if ( has_post_thumbnail( get_the_ID() ) ) {
echo get_the_post_thumbnail( get_the_ID(), 'shop_catalog' );
} else {
echo '<img src="' . esc_url( wc_placeholder_img_src() ) . '" alt="' . esc_attr__( 'Placeholder', 'wpwebguru' ) . '" width="65" height="115">';
}
?>
<h3><?php the_title(); ?></h3>
<?php
echo $product->get_price_html();
woocommerce_template_loop_add_to_cart();
?>
</li>
<?php endwhile; ?>
</ul>
<?php
wp_reset_postdata();
return ob_get_clean();
}
?>
This registers a [custom_loop] shortcode you can place anywhere on your site. It outputs each product’s featured image, title, price, and add-to-cart button inside your own <ul>/<li> structure, ready to style with your own CSS.
Bugs Fixed From the Common Version of This Snippet
A widely-copied version of this code has a few real problems worth understanding, not just copying past:
- Wrong function signature for
woocommerce_template_loop_add_to_cart(). The common version calls it aswoocommerce_template_loop_add_to_cart( $loop->post, $product ). In current WooCommerce, this function takes a single optional$argsarray — it does not accept a post object and a product object as two positional arguments. Passing them this way either throws a PHP error or is silently ignored depending on your WooCommerce version. The function already reads the current product from the global$productvariable (whichthe_post()sets up for you), so calling it with no arguments, as shown above, is both correct and simpler. 'stock' => 1does nothing. That’s not a realWP_Queryparameter — WordPress silently ignores unrecognized array keys, so this line gave the impression of filtering out-of-stock products while actually doing nothing at all. The corrected version above uses a propertax_queryagainst theproduct_visibilitytaxonomy, which is how WooCommerce actually tracks stock status for querying purposes.wp_reset_query()instead ofwp_reset_postdata(). As with other custom-loop snippets on this site,wp_reset_postdata()is the correct cleanup function for a standaloneWP_Queryobject.'showposts' => 6is a long-deprecated alias for'posts_per_page'. It still works via a compatibility shim, but there’s no reason to use the old name in new code.- Unescaped placeholder image markup. The corrected version wraps the fallback image’s
srcinesc_url()and uses the currentwc_placeholder_img_src()function name (the olderwoocommerce_placeholder_img_src()alias still works but is deprecated).
Adjusting the Query for Your Use Case
A few common variations worth knowing:
- Remove the
meta_queryblock entirely if you want all published products, not just featured ones. - Filter by category by adding a
tax_queryentry forproduct_cat— see our guide on getting WooCommerce product categories for the taxonomy details. - Sort by price or popularity by changing
orderbytometa_value_numwithmeta_key => '_price', ormeta_value_numwithmeta_key => 'total_sales'.
Performance Note
Featured-product and best-seller loops like this one are prime candidates for caching — the underlying data usually doesn’t need to be recalculated on every single page load. If this shortcode ends up on a high-traffic page, wrapping the query in a transient (the same pattern covered in our SQL query optimization guide) can meaningfully cut database load on a busy store.
Wrapping Up
The core pattern here — a WP_Query with post_type => 'product', wrapped in a shortcode, outputting your own markup — scales to almost any custom product display you need, from a featured-products carousel to a related-products block. Keep the fixed function calls and proper escaping from the corrected version above whenever you adapt this pattern elsewhere in your store.
2 replies on “How to Make Custom Product Loop in WooCommerce ?”
Your post is very, very, very usefull.
It helps me a lot of.
Thanks
[…] How to Make Custom Product Loop in WooCommerce ? […]