HomeBlogFor developers

Show Random WooCommerce Products from a Category

A WooCommerce shortcode that shows random products from one or more categories, first written in 2018 for a client whose theme overrode WooCommerce's own shortcodes. Corrected and updated in 2026.

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:

php
[woocommerce-custom-category category_id="20" display="4" columns="4"]

Several categories work too: category_id="20,31".

Tell me the problem Call +27 87 150 9305
For your technical person

The code.

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

See the work

Tell me the problem. I’ll tell you honestly what comes next.

+27 87 150 9305

Monday to Friday, 09:00 to 20:00 SAST. Or email info@antondevilliers.com.

Tell me the problem

Step 1 of 3. Pick what’s closest. You can explain in your own words next.

I reply myself, usually the same working day. The first conversation is free, and you’ll know the price of every piece before it starts. How a project works · Privacy

WhatsApp