Show the Sale Discount Amount and Percentage on WooCommerce Products

Quick Summary

  • Filter woocommerce_get_price_html to add a line under sale prices with the amount saved, the percentage, or both, chosen with one setting.
  • To show the percentage in the sale badge, filter woocommerce_sale_flash in classic themes and woocommerce_sale_badge_text in block themes; variable products show their biggest discount.
  • The code uses the product's own price methods, so prices follow your tax display setting and nothing shows outside a scheduled sale.

When a WooCommerce product is on sale, the shop shows the old price crossed out next to the new one, but not how much the customer saves. To show it, filter woocommerce_get_price_html and add a line under the price with the amount, the percentage or both; to put the percentage in the sale badge instead (for example -20% in place of Sale!), filter woocommerce_sale_flash in classic themes and woocommerce_sale_badge_text in block themes. The code for both is below.

Add the code to your child theme’s functions.php file or a code snippets plugin.

Show the amount or percentage saved under the price

PHP
add_filter( 'woocommerce_get_price_html', 'change_displayed_sale_price_html', 10, 2 );
function change_displayed_sale_price_html( $price, $product ) {
    // What to show: 'amount', 'percentage' or 'both'.
    $show      = 'both';
    $precision = 1; // Max number of decimals in the percentage.

    // Only on sale products on the front end, and not on variable products (their price can be a range).
    if ( ( is_admin() && ! wp_doing_ajax() ) || ! $product->is_on_sale() || $product->is_type( 'variable' ) ) {
        return $price;
    }

    // Prices as the shop displays them (including or excluding tax, per your tax settings).
    $regular_price = (float) wc_get_price_to_display( $product, array( 'price' => $product->get_regular_price() ) );
    $sale_price    = (float) wc_get_price_to_display( $product );

    if ( $regular_price <= 0 || $sale_price >= $regular_price ) {
        return $price;
    }

    // "Saving price" and "Saving percentage" calculation and formatting
    $saving_price      = wc_price( $regular_price - $sale_price );
    $saving_percentage = round( 100 - ( $sale_price / $regular_price * 100 ), $precision ) . '%';

    if ( 'amount' === $show ) {
        $saving = $saving_price;
    } elseif ( 'percentage' === $show ) {
        $saving = $saving_percentage;
    } else {
        $saving = $saving_price . ' <em>(' . $saving_percentage . ')</em>';
    }

    // Append to the formatted HTML price
    return $price . '<p class="saved-sale">' . sprintf( 'Save: %s', $saving ) . '</p>';
}

Set $show to what you want to display:

  • 'amount' prints Save: $10.00.
  • 'percentage' prints Save: 20%.
  • 'both' prints Save: $10.00 (20%), with the percentage in <em>.

For a product reduced from $30 to $19.99, the both version printed Save: $10.01 (33.4%) in a test with the prices above.

How it works

  • The woocommerce_get_price_html filter changes the price HTML wherever WooCommerce prints it with get_price_html(): shop and category pages, the single product page, and the Product Price block in block themes.
  • The code only runs for products that are on sale. is_on_sale() also checks the sale dates, so nothing shows before a scheduled sale starts or after it ends.
  • Variable products are left out, because their price can be a range (you can replace that range with a Starting At price). The badge code below handles them.
  • wc_get_price_to_display() returns the regular and sale price the way the shop shows them, including or excluding tax according to your tax settings, so the saving matches the prices next to it. wc_price() formats the difference in your store currency.
  • The percentage is 100 minus the sale price as a percentage of the regular price, rounded to $precision decimals. A product that drops from 50 to 40 gives 20%.
  • The admin area is skipped, but AJAX requests are not: is_admin() is also true during admin-ajax.php requests, which some themes use for quick views and infinite scroll, so wp_doing_ajax() lets those through.
  • The line has the class saved-sale, so you can style it in your theme’s CSS. To change the wording, edit Save: %s.

Show the percentage in the sale badge

Instead of an extra line, you can replace the Sale! badge with the discount. This works for variable products too: they show the biggest discount of any variation, for example Up to -25%.

PHP
// The discount in percent, or 0. For variable products, the biggest discount of any variation.
function get_product_sale_percentage( $product ) {
    if ( $product->is_type( 'variable' ) ) {
        $prices  = $product->get_variation_prices( true );
        $regular = $prices['regular_price'];
        $active  = $prices['price'];
    } else {
        $regular = array( (float) $product->get_regular_price() );
        $active  = array( (float) $product->get_price() );
    }

    $percentage = 0;
    foreach ( $regular as $key => $regular_price ) {
        $active_price = isset( $active[ $key ] ) ? (float) $active[ $key ] : 0;
        if ( $regular_price > 0 && $active_price < $regular_price ) {
            $percentage = max( $percentage, 100 - ( $active_price / $regular_price * 100 ) );
        }
    }
    return round( $percentage );
}

// Classic themes: the "Sale!" badge on shop and product pages.
add_filter( 'woocommerce_sale_flash', 'show_sale_percentage_in_badge', 10, 3 );
function show_sale_percentage_in_badge( $html, $post, $product ) {
    $percentage = get_product_sale_percentage( $product );
    if ( ! $percentage ) {
        return $html;
    }
    $prefix = $product->is_type( 'variable' ) ? 'Up to ' : '';
    return '<span class="onsale">' . esc_html( $prefix . '-' . $percentage . '%' ) . '</span>';
}

// Block themes: the Product Sale Badge block (WooCommerce 10.0 and later).
add_filter( 'woocommerce_sale_badge_text', 'show_sale_percentage_in_badge_block', 10, 2 );
function show_sale_percentage_in_badge_block( $text, $product ) {
    $percentage = get_product_sale_percentage( $product );
    if ( ! $percentage ) {
        return $text;
    }
    return ( $product->is_type( 'variable' ) ? 'Up to ' : '' ) . '-' . $percentage . '%';
}
  • get_product_sale_percentage() compares the regular and active price and rounds the discount to a whole number. For a variable product, get_variation_prices() returns the prices of every visible variation, and the function keeps the biggest discount.
  • Classic themes print the badge from the loop/sale-flash.php and single-product/sale-flash.php templates, only when the product is on sale, and pass it through woocommerce_sale_flash. The new badge keeps the onsale class, so your theme’s badge style still applies.
  • Block themes use the Product Sale Badge block, which has its own filter for the text, woocommerce_sale_badge_text. It was added in WooCommerce 10.0; on older versions that filter does nothing.
  • If a product has no discount to show, for example a grouped product with no regular price of its own, the original badge stays.

This replaces the older approach of reading _regular_price and _sale_price with get_post_meta() in a template: the product methods also work for variable products, and is_on_sale() respects scheduled sales.

Things to check

  • Each snippet declares its functions once. Don’t add the code twice, and remove older versions of these snippets first, or PHP stops with a fatal error because a function is declared twice.
  • If nothing changes, your theme may print its own price or badge instead of WooCommerce’s. Check whether it overrides sale-flash.php in its woocommerce folder.
  • Clear any page cache after adding the code so shop pages show the new line or badge.

If you want sale badges and prices like these designed into a custom store theme, that’s part of my WooCommerce development work.

Share on X (opens in a new tab) Share on LinkedIn (opens in a new tab) Share on Facebook (opens in a new tab)

ChatGPT (opens in a new tab) Claude (opens in a new tab) Gemini (opens in a new tab) Perplexity (opens in a new tab) Grok (opens in a new tab)

More Insights

See More