Skip to content

WooCommerce sale badge: change the text, show the percentage off, and fix it on variable products

Change the WooCommerce "Sale!" badge text, show the percentage saved, make it work in block themes, and fix the missing badge on variable products.

Illustration of a product card whose round badge reads "Save 30%" instead of "Sale!", with a code bracket icon beside it

The default WooCommerce sale badge says "Sale!" and nothing else. You can change the wording with a short filter, show the percentage or amount saved with a slightly longer one, and in block themes you can do most of it from the Site Editor. Variable products need one extra step, because WooCommerce only shows a badge when the parent product counts as on sale. This guide covers all of it, classic and block themes, with copy-and-paste code and a note on when a plugin is the better choice.

Where the badge comes from#

In classic themes, WooCommerce prints the badge through a template (loop/sale-flash.php and single-product/sale-flash.php) and passes the HTML through the woocommerce_sale_flash filter. Anything hooked to that filter can change or replace the badge.

In block themes, product cards built with the Product Collection or Products blocks use the On-Sale Badge block (woocommerce/product-sale-badge). Its text and style live in the Site Editor, and WooCommerce's developer docs describe the block and its filter in the On-Sale Badge block reference.

In both cases the badge appears only when $product->is_on_sale() is true, which is why a mis-saved sale price or a stale schedule can make a badge "disappear". If that is your problem, WooCommerce scheduled sale not working walks through the causes.

Change the "Sale!" text (classic themes)#

Add this to your child theme's functions.php or a code-snippets plugin:

add_filter( 'woocommerce_sale_flash', function ( $html, $post, $product ) {
    return '<span class="onsale">' . esc_html__( 'Deal', 'your-textdomain' ) . '</span>';
}, 10, 3 );

Keep the onsale class so your theme's badge styling still applies.

Show the percentage off#

Shoppers respond to a concrete saving far better than to the word "Sale". For simple products the maths is straightforward:

add_filter( 'woocommerce_sale_flash', function ( $html, $post, $product ) {
    if ( ! $product->is_on_sale() || ! $product->is_type( 'simple' ) ) {
        return $html;
    }
    $regular = (float) $product->get_regular_price();
    $sale    = (float) $product->get_sale_price();
    if ( $regular <= 0 || $sale >= $regular ) {
        return $html;
    }
    $percent = round( ( $regular - $sale ) / $regular * 100 );
    return '<span class="onsale">' . sprintf( esc_html__( 'Save %d%%', 'your-textdomain' ), $percent ) . '</span>';
}, 10, 3 );

To show the amount instead ("Save $30"), replace the last line with wc_price( $regular - $sale ) wrapped in the same sprintf.

Variable products: why the badge is missing, and the "up to" fix#

A variable product is on sale when any of its variations is on sale, so the badge appears on the parent. Two things go wrong in practice:

  1. The badge doesn't show at all. The sale price was set on the parent's General tab, which variable products ignore. Set it on each variation (or use the Variations tab's bulk actions). A long-standing WooCommerce GitHub issue also describes the related quirk where a variable product shows no crossed-out range when only one variation is discounted.
  2. The percentage snippet above does nothing. It only handles simple products, because a variable product has many prices. The honest label is the largest discount across its variations, as "Up to X% off":
add_filter( 'woocommerce_sale_flash', function ( $html, $post, $product ) {
    if ( ! $product->is_on_sale() || ! $product->is_type( 'variable' ) ) {
        return $html;
    }
    $max = 0;
    foreach ( $product->get_available_variations( 'objects' ) as $variation ) {
        $regular = (float) $variation->get_regular_price();
        $sale    = (float) $variation->get_sale_price();
        if ( $variation->is_on_sale() && $regular > 0 && $sale < $regular ) {
            $max = max( $max, round( ( $regular - $sale ) / $regular * 100 ) );
        }
    }
    if ( ! $max ) {
        return $html;
    }
    return '<span class="onsale">' . sprintf( esc_html__( 'Up to %d%% off', 'your-textdomain' ), $max ) . '</span>';
}, 10, 3 );

Keep "up to" in the wording. Stating "30% off" on a product where only one size is 30% off is the kind of claim consumer regulators look at; see sale pricing rules for WooCommerce stores.

Block themes: edit the badge in the Site Editor#

  1. Go to Appearance → Editor → Templates and open the template that lists products (for example Product Catalog) or the Single Product template.
  2. Select the On-Sale Badge block inside the product card. Change its text, alignment, colours and padding in the block settings.
  3. Save. The change applies wherever that template is used.

The block's default text can also be changed in code with the woocommerce_sale_badge_text filter described in the developer reference linked above. A percentage badge, as of this writing, still needs a snippet or a plugin; there is an open request to add "% off" to the core badge, which is a good signal of how often store owners want it.

Style the badge#

Most themes style .onsale already. To make it a round, high-contrast badge:

.onsale {
  position: absolute; top: 12px; left: 12px;
  background: #FF632C; color: #fff;
  font-weight: 800; font-size: 13px; line-height: 1;
  padding: 10px 12px; border-radius: 999px;
  min-width: 0; min-height: 0;
}

Test on a product page and in the grid, on mobile, because themes position the two differently.

Do you need a plugin?#

A badge plugin earns its place when you want multiple badges with rules ("New", "Low stock", "Best seller"), scheduled badge campaigns, image badges, or a settings screen a non-developer can use. For "say the saving instead of Sale!", the snippets above are enough, and they add no weight to your pages.

The badge solves half the problem#

A better badge helps the shopper who is already looking at a sale product. It does nothing for the visitor reading a blog post or arriving on a full-price product page. For that, the sale needs to travel with the shopper: see how to show sale products on every page.

Sales Drawer takes the same "say the saving" idea site-wide: every product in the drawer shows its regular price, its sale price and the saving, and you can choose the layout and colours to match your badge.

Frequently asked questions#

How do I change the "Sale!" text in WooCommerce?#

In classic themes, hook the woocommerce_sale_flash filter and return your own <span class="onsale"> markup. In block themes, edit the On-Sale Badge block in the Site Editor.

How do I show the percentage off on the sale badge?#

Use a woocommerce_sale_flash filter that reads the product's regular and sale price, calculates the percentage, and returns it in the badge. For variable products, calculate the largest discount across variations and label it "Up to X% off".

Why is the sale badge not showing on my variable product?#

Usually because the sale price was entered on the parent product instead of on each variation. Set the sale price (and any schedule) on the variations, then check the product again. If only some variations are on sale, the badge still shows, but the crossed-out price range may not.

Does the sale badge work with block themes?#

Yes. Product grids built with the Product Collection or Products blocks use the On-Sale Badge block, which you can edit in the Site Editor. The classic woocommerce_sale_flash filter does not affect block-based product cards.

Do I need a plugin to customise the sale badge?#

Not for text or percentage changes; a short snippet handles those. A plugin makes sense when you want several badge types with rules, image badges or a settings screen.

Why has my sale badge disappeared?#

The product is no longer considered on sale: the sale period ended, the sale price was removed or isn't lower than the regular price, or a scheduled sale hasn't switched on yet. Check the product's price fields and WooCommerce → Status → Scheduled Actions.