I wrote this in 2018 for a client on the Avada theme. Avada replaces most of WooCommerce's product shortcodes with its own, and the one they needed, a few random products from a chosen category, lost the parameters that made it work. So I wrote a small shortcode of my own. The version below is corrected and updated in 2026: the 2018 version had bugs, including an example that quietly ignored the category. Add it to your child theme's functions.php or a small plugin.
Usage
Four random products from category 20, in four columns:
[woocommerce-custom-category category_id="20" display="4" columns="4"]
Several categories work too: category_id="20,31".
For your technical person
The code.
add_shortcode( 'woocommerce-custom-category', 'woocommerce_custom_category_shortcode' );
/**
* Random products from one or more categories.
* category_id: comma-separated product category IDs
* display: how many products to show
* columns: how many columns
*/
function woocommerce_custom_category_shortcode( $atts ) {
$atts = shortcode_atts( array(
'category_id' => '',
'display' => '4',
'columns' => '4',
), $atts, 'woocommerce-custom-category' );
$category_ids = array_filter( array_map( 'absint', explode( ',', $atts['category_id'] ) ) );
if ( empty( $category_ids ) ) {
return '';
}
$query = new WP_Query( array(
'post_type' => 'product',
'post_status' => 'publish',
'posts_per_page' => absint( $atts['display'] ),
'orderby' => 'rand',
'ignore_sticky_posts' => 1,
'tax_query' => array(
array(
'taxonomy' => 'product_cat',
'field' => 'term_id',
'terms' => $category_ids,
'operator' => 'IN',
),
),
) );
if ( ! $query->have_posts() ) {
return '';
}
ob_start();
?>
<div class="woocommerce columns-<?php echo esc_attr( absint( $atts['columns'] ) ); ?>">
<ul class="products columns-<?php echo esc_attr( absint( $atts['columns'] ) ); ?>">
<?php while ( $query->have_posts() ) : $query->the_post(); ?>
<?php $product = wc_get_product( get_the_ID() ); ?>
<?php if ( ! $product ) { continue; } ?>
<li <?php wc_product_class( '', $product ); ?>>
<a class="product-images" href="<?php echo esc_url( get_permalink() ); ?>">
<?php echo $product->get_image( 'woocommerce_thumbnail' ); ?>
</a>
<h3 class="product-title">
<a href="<?php echo esc_url( get_permalink() ); ?>"><?php the_title(); ?></a>
</h3>
<?php if ( '' !== $product->get_price() ) : ?>
<span class="price"><?php echo $product->get_price_html(); ?></span>
<?php endif; ?>
<?php woocommerce_template_loop_add_to_cart(); ?>
</li>
<?php endwhile; ?>
</ul>
</div>
<?php
wp_reset_postdata();
return ob_get_clean();
}
What changed from the 2018 version. The example now passes category_id (the old example used id, which the function never read, so it showed products from any category). add_shortcode has its semicolon. The function is a plain function, not public function, since it doesn't live in a class. Deprecated calls are gone (product_type, the shop_catalog image size). The price markup sits entirely inside its check, so a product without a price no longer breaks the HTML. Output buffering starts before any output, and the query is reset before the function returns.
wc_product_class() and woocommerce_template_loop_add_to_cart() rely on WooCommerce setting the global $product on the_post, which it does inside a standard WP_Query loop. wc_product_class() needs WooCommerce 3.4 or later.
One caution. orderby => rand is fine for small and medium catalogues. On a store with tens of thousands of products it gets slow, and a cached random selection is the better approach.
