Show sale percentage and savings on WooCommerce products

Sales are a great tool store owners have at their disposal to attract customer interest and sell more products. WooCommerce of course offers the ability to put products on sale by adding a sale price along with the product’s regular price. Then on the front end it makes sure the customer notices on-sale products by adding a sale badge with the word SALE on it and showing the regular price with a strike-through next to the sale price.

What if we were to make this a bit more intriguing to your customers though? How about instead of just a simple word, we display the percentage of the discount on the sale badge, and additionally we display a small text next to the single product price with the amount of money the user saves while the product is on sale? Sounds good? Let’s see how we can achieve it.

Install and activate a child theme

The first step on our process here is to create and install a child theme. If you are using one of our themes you can easily grab its child theme from our downloads section. If not, you can read our beginner’s guide on child themes to create your own. This step is essential in order to preserve our changes throughout theme updates.

All code below will be placed in our child theme’s functions.php file. If the file is empty the code will go just below the opening <?php tag, otherwise, if it has contents, the code should be placed at the end of the file, before the closing ?> PHP tag if it exists.

Replace the SALE banner

If you are using one of our Ignition Framework based themes you can skip this section, this functionality is already built into the framework and can be enabled under Customizer > WooCommerce > Product Catalog > Show the percentage of discount on the sale badge.

Below you will find a screenshot of on sale products as they appear by default on the shop listing page of the Storefront theme.

We start by filtering the output of the WooCommerce’s sale flash banner, with the following function:

add_filter( 'woocommerce_sale_flash', 'cssigniter_woocommerce_sale_flash_percentage', 10, 3 );
/**
 * Replaces the default "Sale!" badge text with the percentage of discount.
 * Returns the HTML code that contains the default "Sale!" badge text, replaced with the percentage of discount.
 *
 * @param string     $html
 * @param WP_Post    $post
 * @param WC_Product $product
 *
 * @return string
 */
function cssigniter_woocommerce_sale_flash_percentage( $html, $post, $product ) {

	$found = preg_match( '#(<span.*?>)(.*?)(</span>)#', $html, $matches );

	if ( ! $found ) {
		return $html;
	}

	$tag_open      = $matches[1];
	$tag_close     = $matches[3];
	$original_text = $matches[2];

	$percentages = cssigniter_woocommerce_get_product_sale_percentages( $product );
	$label       = cssigniter_woocommerce_get_product_sale_percentage_label( $percentages, $original_text );

	$html = $tag_open . $label . $tag_close;

	return $html;
}
Filter the output of the sale banner.

In this function we use a regular expression to find and store the opening and closing tags of the sale flash banner, along with its original content. Lines 24 & 25 is where most of the work is done through two helper functions.

/**
 * Returns a product's discount as a set of minimum and maximum percentages.
 *
 * Compound products, such as grouped and variable products, may have multiple different discount percentages due to
 * on-sale children products or variations. In these cases, both 'min' and 'max' values are set, although may be the same.
 *
 * For simple products, run the returned array through max() to get the correct value.
 *
 * @param WC_Product $product
 *
 * @return array {
 *     @type float|false $min
 *     @type float|false $max
 * }
 */
function cssigniter_woocommerce_get_product_sale_percentages( $product ) {
	$percentages = array(
		'min' => false,
		'max' => false,
	);

	switch ( $product->get_type() ) {
		case 'grouped':
			$children = array_filter( array_map( 'wc_get_product', $product->get_children() ), 'wc_products_array_filter_visible_grouped' );

			foreach ( $children as $child ) {
				if ( $child->is_purchasable() && ! $child->is_type( 'grouped' ) && $child->is_on_sale() ) {
					$child_percentage = cssigniter_woocommerce_get_product_sale_percentages( $child );

					$percentages['min'] = false !== $percentages['min'] ? $percentages['min'] : $child_percentage['min'];
					$percentages['max'] = false !== $percentages['max'] ? $percentages['max'] : $child_percentage['max'];

					if ( $child_percentage['min'] < $percentages['min'] ) {
						$percentages['min'] = $child_percentage['min'];
					}

					if ( $child_percentage['max'] > $percentages['max'] ) {
						$percentages['max'] = $child_percentage['max'];
					}
				}
			}

			break;

		case 'variable':
			$prices = $product->get_variation_prices();

			foreach ( $prices['price'] as $variation_id => $price ) {
				$regular_price = (float) $prices['regular_price'][ $variation_id ];
				$sale_price    = (float) $prices['sale_price'][ $variation_id ];

				if ( $sale_price < $regular_price ) {
					$variation_percentage = ( ( $regular_price - $sale_price ) / $regular_price ) * 100;

					$percentages['min'] = false !== $percentages['min'] ? $percentages['min'] : $variation_percentage;
					$percentages['max'] = false !== $percentages['max'] ? $percentages['max'] : $variation_percentage;

					if ( $variation_percentage < $percentages['min'] ) {
						$percentages['min'] = $variation_percentage;
					}

					if ( $variation_percentage > $percentages['max'] ) {
						$percentages['max'] = $variation_percentage;
					}
				}
			}
			break;

		case 'external':
		case 'variation':
		case 'simple':
		default:
			$regular_price = (float) $product->get_regular_price();
			$sale_price    = (float) $product->get_sale_price();

			if ( $sale_price < $regular_price ) {
				$percentages['max'] = ( ( $regular_price - $sale_price ) / $regular_price ) * 100;
			}
	}

	return $percentages;
}
Return a product's discount as a set of minimum and maximum percentages.

The first function returns the percentage of the product’s discount. This could be a range of percentages because grouped and variable products can have different sale percentages per grouped product or variation.

/**
 * Returns the percentage text to be displayed on the sale badge.
 *
 * @param array $percentages
 *
 * @return string
 */
function cssigniter_woocommerce_get_product_sale_percentage_label( $percentages, $original_label ) {
	$label = '';

	$rounded_percentages = $percentages;
	$rounded_percentages = array_map( 'round', $percentages );
	$rounded_percentages = array_map( 'intval', $rounded_percentages );

	if ( ( empty( $percentages['min'] ) || empty( $percentages['max'] ) ) || ( $percentages['min'] === $percentages['max'] ) ) {
		/* translators: %1$d is a discount percentage. E.g. -60% */
		$label = sprintf( _x( '-%1$d%%', 'product discount', 'your-text-domain' ), max( $rounded_percentages ) );
	} else {
		/* translators: The whole string is a discount range. %1$d is the minimum discount percentage, %2$d is the maximum discount percentage. E.g. -10% / -60% */
		$label = sprintf( _x( '-%1$d%% / -%2$d%%', 'product discount', 'your-text-domain' ), $rounded_percentages['min'], $rounded_percentages['max'] );
	}

	/**
	 * Filters the sale flash's percentage-based label.
	 *
	 * @param string $label The sale flash's label text.
	 * @param array $rounded_percentages {
	 *     Array of rounded sale percentages' extremes.
	 *
	 *     @type int $min
	 *     @type int $max
	 * }
	 * @param array $percentages {
	 *     Array of sale percentages' extremes.
	 *
	 *     @type float $min
	 *     @type float $max
	 * }
	 * @param string $original_label The sale flash's original label text.
	 */
	$label = apply_filters( 'cssigniter_woocommerce_sale_flash_percentage_label', $label, $rounded_percentages, $percentages, $original_label );

	return $label;
}
Return the percentage text to be displayed on the sale badge.

The second function cleans up the percentage values received by the previous function and generates the text that will be placed in the sale flash banner.

In line 27 of cssigniter_woocommerce_sale_flash_percentage above, the banner’s html is rebuilt and returned through the filter.

After we apply our code the sale flash banner will change to reflect the percentage of sale for each product.

Show savings along with the single product’s price

What we are going to do next is display the amount of money a customer saves for products on sale. While the regular and sale prices are visible on the single product view, it might take more than a couple of seconds for the visitor to calculate the exact amount of money saved, which might cause them to gloss over the sale, we don’t want that, so let’s calculate it for them and display it along with the price.

To achieve our goal we will use a slightly modified version of the cssigniter_woocommerce_get_product_sale_percentages, function used above.

add_action( 'woocommerce_single_product_summary', 'cssigniter_woocommerce_get_product_sale_savings', 11 );
/**
 * Returns a textual string with the amount saved for on-sale products.
 *
 * Compound products, such as grouped and variable products, may have multiple different savings amounts due to
 * on-sale children products or variations. In these cases, both 'min' and 'max' values are set, although may be the same.
 *
 * @return string
 */
function cssigniter_woocommerce_get_product_sale_savings() {
	global $product;

	if ( ! $product->is_on_sale() ) {
		return;
	}

	$savings = array(
		'min' => false,
		'max' => false,
	);

	$savings_html = '';

	switch ( $product->get_type() ) {
		case 'grouped':
			$children = array_filter( array_map( 'wc_get_product', $product->get_children() ), 'wc_products_array_filter_visible_grouped' );

			foreach ( $children as $child ) {
				if ( $child->is_purchasable() && ! $child->is_type( 'grouped' ) && $child->is_on_sale() ) {
					$child_regular_price = (float) wc_get_price_to_display( $child, array( 'price' => $child->get_regular_price() ) );
					$child_active_price  = (float) wc_get_price_to_display( $child, array( 'price' => $child->get_sale_price() ) );
					$child_savings       = $child_regular_price - $child_active_price;

					$savings['min'] = false !== $savings['min'] ? $savings['min'] : $child_savings;
					$savings['max'] = false !== $savings['max'] ? $savings['max'] : $child_savings;

					if ( $child_savings < $savings['min'] ) {
						$savings['min'] = $child_savings;
					}

					if ( $child_savings > $savings['max'] ) {
						$savings['max'] = $child_savings;
					}
				}
			}

			break;

		case 'variable':
			$prices = $product->get_variation_prices();

			foreach ( $prices['price'] as $variation_id => $price ) {
				$regular_price = (float) $prices['regular_price'][ $variation_id ];
				$sale_price    = (float) $prices['sale_price'][ $variation_id ];

				if ( $sale_price < $regular_price ) {
					$variation_savings = $regular_price - $sale_price;

					$savings['min'] = false !== $savings['min'] ? $savings['min'] : $variation_savings;
					$savings['max'] = false !== $savings['max'] ? $savings['max'] : $variation_savings;

					if ( $variation_savings < $savings['min'] ) {
						$savings['min'] = $variation_savings;
					}

					if ( $variation_savings > $savings['max'] ) {
						$savings['max'] = $variation_savings;
					}
				}
			}

			break;

		case 'external':
		case 'variation':
		case 'simple':
		default:
			$regular_price = (float) $product->get_regular_price();
			$sale_price    = (float) $product->get_sale_price();

			if ( $sale_price < $regular_price ) {
				$savings['max'] = $regular_price - $sale_price;
			}
	}

	if ( ! empty( $savings['min'] ) ) {
		$savings_html = '<p class="saving_total_price">' . __( 'Save from ', 'your-text-domain' ) . wc_price( $savings['min'] ) . ' up to ' . wc_price( $savings['max'] ) . ' ' . __( 'per item.', 'your-text-domain' ) . '</p>';
	} else {
		$savings_html = '<p class="saving_total_price">' . __( 'You save ', 'your-text-domain' ) . wc_price( $savings['max'] ) . '</p>';
	}

	echo wp_kses_post( $savings_html );
}
Create a string containing the minimum and maximum amount of savings where applicable.

Our function here starts by checking the type of the current product. If it encounters a variable or grouped product it calculates and stores the minimum and maximum amounts of money saved and stores it, for the rest of the product types it just calculates the savings from the product sale. The stored values are used to create the string which will be output below the price on the single product view.

This is what our single product view will look after the code is applied.

Potential customers now have immediate access to the amount of money saved on each product.

The generated string offers the saving_total_price class for styling, you can use it to apply any CSS of your choosing to make the text stand out more. You can even replace it with the woocommerce-notice one to take advantage of built in WooCommerce styling for important information, this is how it would look on our case with the woocommerce-notice class applied.

Now this is guaranteed to draw the visitor’s attention.

Similarly a range of savings will be displayed when a user visits a grouped or variable product.

Wrapping up

We have successfully replaced the SALE text in the sale flash badge with the sale percentage for each product and we have made immediately available the amount of money saved on on-sale products to our customers with the help of a child theme and a few lines of code. These simple changes will make sales more appealing to your potential customers and could easily help increase your revenue. If you end up using any of these snippets let us know of your experience with them and the impact they had on your store in the comments below.

Related Articles

Leave a Reply

Your email address will not be published. Required fields are marked *