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
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_htmlfilter changes the price HTML wherever WooCommerce prints it withget_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
$precisiondecimals. 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 duringadmin-ajax.phprequests, which some themes use for quick views and infinite scroll, sowp_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%.
// 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.phpandsingle-product/sale-flash.phptemplates, only when the product is on sale, and pass it throughwoocommerce_sale_flash. The new badge keeps theonsaleclass, 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.phpin itswoocommercefolder. - 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.
Related articles
- replacing the variable price range with Starting At: another way to change how WooCommerce prints prices
- frequently used WooCommerce codes: printing product prices and other product data in your own templates
