WooCommerce Tips

The WooCommerce Discount That Doesn’t Show a Sale Badge (And the Ones That Do)

The WooCommerce Discount That Doesn't Show a Sale Badge (And the Ones That Do)


WooCommerce Troubleshooting

The WooCommerce Discount That Doesn’t Show a Sale Badge (And the Ones That Do)

You applied a discount and the product price dropped. But the “Sale!” badge never appeared on the product image. Whether that’s a problem — and how to fix it — depends entirely on which discount mechanism you used.

You set up a WooCommerce discount. The price dropped on the product page. But there’s no “Sale!” sticker on the product image, no strikethrough on the old price, no visual signal that anything is different. Customers see the reduced price but have no idea they’re getting a deal.

Or the opposite happens: the badge is there, but a customer calls in to say the discount isn’t working — because they applied it as a coupon code and expected a badge before checkout.

Both situations come down to the same root issue: different discount mechanisms in WooCommerce work at different layers of the system, and the “Sale!” badge only responds to one of those layers. Understanding which mechanism you’re using is the key to diagnosing what’s missing.

How the “Sale!” badge works in WooCommerce

WooCommerce’s “Sale!” badge is output by the woocommerce_show_product_loop_sale_flash and woocommerce_show_product_sale_flash template actions. Internally they call wc_get_sale_flash(), which checks whether the product is currently on sale by calling $product->is_on_sale().

That method — is_on_sale() — compares the product’s current sale price with its regular price. Specifically, it calls get_sale_price() and checks whether the result is a non-empty value that’s less than the regular price.


The key method

$product->is_on_sale() is what the badge checks. If this method returns true at render time, the badge appears. If it returns false, it doesn’t. What determines that return value depends entirely on how your discount was applied.

The critical detail: get_sale_price() is a filterable method. It fires the woocommerce_product_get_sale_price filter, which means plugins can intercept it and return a modified value. Whether that happens — and what it returns — determines whether the badge renders.

Native sale price: always shows a badge

When you open a product in WooCommerce admin and fill in the Sale price field under Product Data → General, WooCommerce saves that value to the product’s _sale_price postmeta record. When the product is later loaded on the storefront, get_sale_price() reads that stored value directly from the database. No filters needed — the value is just there.

If that stored value is non-empty and less than the regular price, is_on_sale() returns true, and the badge renders. The strikethrough price renders too, because WooCommerce’s price HTML formatting shows crossed-out regular price alongside the sale price whenever is_on_sale() is true.

This is the most reliable path to a sale badge. It also requires the most manual effort — someone has to set the sale price on each product, and clear it when the promotion ends. For large catalogs or time-limited promotions, this process doesn’t scale. That’s why most stores eventually move to a discount plugin rather than editing products manually. The tradeoffs in that shift are explored in the post on why running WooCommerce sales still takes hours when you’re doing it manually.

Scheduled sale prices (WooCommerce native)

WooCommerce also lets you schedule a sale price with a From/To date range on the product edit page. When the schedule starts, WooCommerce uses a cron job to activate the sale price (it sets the active price to the sale price). When the schedule ends, another cron event clears it. The badge behavior is identical: the badge appears while the stored sale price is active, and disappears when the schedule ends and the sale price is cleared.

The catch with scheduled native sale prices is that the cron-based activation can lag if your site has low traffic. WP-Cron only fires when someone visits the site, so a sale scheduled for midnight might not actually start until the first visitor after midnight. This is covered in more detail in the post on why WooCommerce scheduled sales don’t start on time.

WooCommerce coupons: never show a badge on the product

WooCommerce coupons are the most widely used discount mechanism, and the one most likely to surprise people who expect a badge: coupons never show a “Sale!” badge on the product page, category page, or anywhere else on the storefront before checkout.

The reason is architectural. A coupon is applied at the cart level. The discount doesn’t exist until a shopper adds a product to their cart and enters a coupon code at checkout (or the coupon is applied automatically). Before that moment, there is nothing for the product page to show. The product’s regular price is still its regular price — the coupon hasn’t been applied yet.

Nothing in WooCommerce’s coupon system touches get_sale_price() or is_on_sale() for individual products. The discount is calculated in the cart, applied to the cart totals, and shown on the cart and checkout pages. It does not reach back to product pages.


Coupons work, but stay hidden until checkout

If your customers are asking “I have a coupon code but the price isn’t showing the discount” — they’re right that the product page price hasn’t changed. That’s expected. The coupon will apply at checkout. This is a communication problem, not a bug. Make sure your checkout page or cart makes the savings visible when the code is entered.

What coupons do show

When a coupon is applied in the cart or checkout, WooCommerce displays the discount as a line in the cart totals (e.g. “Coupon: SUMMER20 — −$10.00”). It does not modify the individual product’s displayed price — the line item price stays at the regular price, and the total reflects the deduction.

For stores that rely heavily on coupon codes as the primary promotion mechanism, this is a genuine UX consideration. Shoppers have to trust that the discount will apply at checkout without any indication on the product pages that a deal exists. Some stores add a banner or call-to-action instead to communicate the offer.

Runtime discount plugins: badge usually shows, On Sale filter doesn’t

Most WooCommerce discount plugins — plugins that automate sale pricing across products using rules or campaigns — work by hooking into WooCommerce’s pricing filter hooks rather than writing to the database. When a product page is loaded, the plugin intercepts woocommerce_product_get_sale_price and woocommerce_product_get_price, calculates the discount, and returns the modified price.

Because get_sale_price() fires those filters, is_on_sale() sees the discounted value and returns true. WooCommerce then renders the “Sale!” badge and strikethrough price — both appear automatically without the plugin needing to do any special badge work. The badge is a consequence of the modified get_sale_price() result, not a separate feature.

This means the sale badge behavior for runtime discount plugins is good news: the badge works. The product page looks correct. The discount is visible.

The less good news is the part that doesn’t work: because the plugin never wrote anything to the database, WooCommerce’s on-sale product lookup infrastructure — the wc_get_product_ids_on_sale() function and the wc_product_meta_lookup.onsale column — have no knowledge of the discount. Those tools query the database, not the result of PHP filters. So “On Sale” shop filters (FacetWP, JetSmartFilters, and others) come up empty for discounted products, even when the product page badge is correct. That specific issue is covered in depth in the post on why discounted WooCommerce products don’t show up in the On Sale shop filter.

How Smart Cycle Discounts handles the sale badge

Smart Cycle Discounts applies discounts at display time through WooCommerce’s price filter hooks. Specifically, it hooks woocommerce_product_get_price, woocommerce_product_get_sale_price, woocommerce_product_variation_get_price, and woocommerce_product_variation_get_sale_price. When one of these fires, Smart Cycle Discounts checks whether the current product has an active campaign, calculates the discounted price, and returns it.

Because woocommerce_product_get_sale_price is filtered, get_sale_price() returns the discounted value at render time. is_on_sale() returns true. WooCommerce renders its default “Sale!” badge and strikethrough price on shop pages, category pages, product pages, and search results — automatically, without any extra configuration in Smart Cycle Discounts. No database writes happen. No product records are modified.


The badge shows by default

When a Smart Cycle Discounts campaign is active and reduces a product’s price, WooCommerce’s default “Sale!” badge renders automatically. This happens on every theme that renders the standard sale flash template hooks. No special settings are needed to enable it.

The optional badge suppression setting

Smart Cycle Discounts 2.1.2 added the Promotional Visuals system — a live designer for custom sale badges that can replace the theme’s default “Sale!” sticker with a branded custom design. When you assign a Promotional Visual image badge to a campaign, you may end up with two badges appearing simultaneously: the theme’s default sticker and your custom one.

To prevent that, Smart Cycle Discounts includes a “Hide theme sale badge” option in Display Settings (under Smart Cycle Discounts → Settings → Display). When both “Enable badges” and “Hide theme sale badge” are turned on, the plugin hooks woocommerce_sale_flash and suppresses the theme sticker — but only for products that have a Smart Cycle Discounts badge assigned and active. Products without an SCD badge continue to show the theme’s own badge unmodified.

This means: if a customer reports that the “Sale!” badge disappeared after you turned on the Promotional Visuals system, the most likely cause is that “Hide theme sale badge” is enabled but the relevant campaign doesn’t have a custom badge assigned. The setting suppresses the theme badge in anticipation of a custom one, but no custom one is there. The fix is either to assign a Promotional Visual badge to the campaign, or to turn off “Hide theme sale badge” if you don’t want custom badges.

For the full walkthrough of the Promotional Visuals designer — image badges, summary badges, BOGO panels, and more — the post on designing WooCommerce sale badges with Smart Cycle Discounts Promotional Visuals covers every step.

What Smart Cycle Discounts does not do

To be precise about the tradeoffs: Smart Cycle Discounts does not write the discounted price to the product’s _sale_price postmeta field in the database. The discount exists entirely at display time, in PHP memory, as a result of the price filter returning a modified value. When the campaign ends or is deactivated, the filter stops returning a modified value, and the product price returns to normal — instantly, with no cleanup work needed.

The consequence is the one covered in the On Sale filter post: WooCommerce’s catalog-level on-sale queries don’t find these products, because those queries read the stored _sale_price field, not the result of per-product filters. The badge shows correctly on the product page, but a FacetWP “On Sale” facet, a YITH filter, or the WooCommerce On Sale block won’t include them.

When the badge is genuinely missing: a diagnosis checklist

If you have a discount running and the badge is not appearing, work through this checklist before assuming a plugin conflict or bug.

Step 1: Identify which discount mechanism you’re using

Is the discount applied via a coupon code? Then the badge is not supposed to appear on the product page. That is expected behavior. The discount lives at the cart level.

Is it applied via a WooCommerce native sale price? Check the product in WooCommerce admin → Product Data → General. Is the Sale price field populated? If not, nothing will trigger the badge.

Is it applied via a discount plugin? Continue to step 2.

Step 2: Check whether the plugin hooks the sale price filter

For a discount plugin to trigger the badge, it must hook woocommerce_product_get_sale_price (or the variation equivalent) and return the discounted value from that filter. If your plugin only hooks the total price filter (woocommerce_product_get_price) but not the sale price filter, get_sale_price() still returns the original empty value, is_on_sale() returns false, and no badge appears.

The test: on an active discount’s product page, right-click → View Page Source, and search for the sale badge HTML (typically class="onsale"). If it’s absent, the sale price filter isn’t returning a discounted value.

Step 3: Check the plugin’s context guards

Some discount plugins apply prices only on specific pages (cart and checkout) and intentionally skip the get_sale_price() filter on shop and product pages. This is a design choice, not a bug — usually done to keep the displayed price clean before cart context (like a quantity or cart total) is known. In these cases, the product page price won’t show a discount, and no badge will appear, by design.

Check the plugin’s documentation for which pages and hooks it applies pricing changes to. Some plugins distinguish between “display price” and “cart price” and deliberately don’t modify the display price.

Step 4: Check for badge suppression settings

If you’re using Smart Cycle Discounts and the badge was showing but has now disappeared, check Display Settings for the “Hide theme sale badge” toggle. If it’s on and the affected products don’t have a custom Promotional Visual badge assigned, the theme badge will be suppressed with nothing to replace it.

Also check whether another plugin is filtering woocommerce_sale_flash. Any plugin that returns an empty string from this filter will suppress the badge globally or for specific products. Query Monitor’s Hooks tab will show all callbacks registered for this filter.

Step 5: Check for theme-specific badge templates

Some themes override WooCommerce’s default badge template using a custom woocommerce/loop/sale-flash.php template in the theme folder. If a theme has modified the conditions under which the badge renders, or suppresses it entirely, no plugin will trigger it. Look in your theme’s woocommerce/loop/ folder for a sale-flash.php override and compare it with WooCommerce’s default version.

Code-gated campaigns: a special case

Some campaign-based discount plugins — including Smart Cycle Discounts — support “code-gated” campaigns: discounts that only activate when the shopper has entered a specific campaign code. This is different from a standard WooCommerce coupon: the code triggers a full campaign discount (percentage off, tiered pricing, BOGO) rather than a simple cart deduction.

For code-gated campaigns, the behavior mirrors coupon behavior at the product page level: the badge does not appear on the product page, because the discount hasn’t been unlocked yet. The product’s sale price filter won’t return a discounted value until the code is in the cart. Once the code is applied, Smart Cycle Discounts activates the campaign for that cart session, and the discount takes effect at cart/checkout — but not as a product-level badge.

This is the intended behavior for that delivery mode. If a campaign is meant to be a public, no-code discount visible on product pages, use the standard (non-code-gated) campaign mode instead. For a detailed look at the mechanics of code-gated campaigns versus always-on campaigns, see the post on WooCommerce code-gated discount campaigns vs. auto-apply.

Badge showing vs. appearing in the On Sale filter: two different things

It’s worth being clear about a distinction that catches a lot of store owners off guard: the sale badge showing on the product page and the product appearing in WooCommerce’s “On Sale” shop filter are two entirely separate questions.

The badge is determined by $product->is_on_sale(), which fires per-product filter hooks at request time. A runtime discount plugin that modifies get_sale_price() triggers the badge correctly.

The On Sale shop filter is determined by wc_get_product_ids_on_sale(), which reads the wc_product_meta_lookup.onsale database column. This column is only set when a product has a database-resident _sale_price. Runtime discount plugins that apply prices through filters — without writing to the database — don’t set this column. So the badge shows, but the filter doesn’t find the product.

Discount mechanism “Sale!” badge on product page Appears in On Sale shop filter Visible before cart
Native WooCommerce sale price Yes Yes Yes
WooCommerce coupon code No No No
Runtime discount plugin (e.g. SCD auto-apply campaign) Yes No Yes
SCD code-gated campaign No No No
Database-write discount plugin Yes Yes Yes

If your store uses a shop filter and you need discounted products to appear in it, the database-write approach is the only one that works without custom code. Runtime filter-based plugins — including Smart Cycle Discounts and most other rule-engine discount plugins — produce a correct badge but are invisible to catalog-level on-sale queries. The full architecture explanation is in the On Sale filter post.

Frequently asked questions

Why doesn’t my WooCommerce coupon show a sale badge on the product page?

WooCommerce coupons apply at the cart level, not the product level. The discount is calculated when a shopper enters the code at cart or checkout — it does not modify individual product prices or trigger $product->is_on_sale() on product pages. The badge requires a non-empty sale price to be returned for the product at render time; coupons never produce that. This is expected WooCommerce behavior, not a bug.

I set up a Smart Cycle Discounts campaign but the Sale badge disappeared. What happened?

The most likely cause is the “Hide theme sale badge” setting in Smart Cycle Discounts Display Settings. When this is on alongside “Enable badges,” Smart Cycle Discounts suppresses the WooCommerce theme badge for products with an active SCD campaign. If the campaign doesn’t have a custom Promotional Visual badge assigned, the theme badge is suppressed with nothing to replace it. Either assign a Promotional Visual image badge to the campaign, or disable “Hide theme sale badge” if you prefer the theme’s default sticker.

Does a WooCommerce discount plugin always show a sale badge?

Not always. For a discount plugin to trigger the badge, it needs to return a discounted value from the woocommerce_product_get_sale_price filter. Plugins that only hook the price filter (not the sale price filter), or that intentionally skip product-page price modifications to handle cart-level context-dependent discounts, will not produce a badge on product pages. Check whether the plugin specifically mentions “product page” or “display price” modification in its documentation, not just “cart discount.”

The sale badge shows on the product page, but my On Sale shop filter is empty. How are those two things different?

They read from different sources. The sale badge is based on $product->is_on_sale(), which fires WooCommerce price filters at request time and can see a runtime discount. The On Sale shop filter uses wc_get_product_ids_on_sale(), which reads the wc_product_meta_lookup.onsale database column — a column that’s only set when a product has a stored _sale_price in the database. Runtime filter-based discount plugins modify prices in memory but never write to the database, so the badge shows but the filter comes up empty. This is the core mismatch described in the On Sale filter guide.

What’s the difference between a sale badge and a promotional visual badge in Smart Cycle Discounts?

The WooCommerce theme’s default “Sale!” badge is a simple label generated by the theme whenever is_on_sale() returns true. It shows the word “Sale!” (or a translated equivalent) with no detail about the actual discount. A Smart Cycle Discounts Promotional Visual image badge is a custom designed element — you control the shape, color, text, icon, and position. It can show dynamic values like the actual discount percentage using tokens like {discount_value}. When you use a Promotional Visual badge, you typically turn on “Hide theme sale badge” so only your custom design shows, not both.

Can I make WooCommerce show a sale badge for a coupon discount?

Not through standard WooCommerce mechanisms. Coupons are cart-level, and the product-page badge system has no way to know in advance which products a coupon will discount (especially for percentage-off or category-specific coupons). You can work around this by adding a manual sale price to the product to trigger the badge independently of the coupon, but then you’re managing two separate price layers. Alternatively, use a banner or announcement bar to communicate the coupon offer instead of relying on a per-product badge.


Key takeaways

  • Native sale prices always show a badge. WooCommerce stores the price in _sale_price postmeta, and is_on_sale() reads it directly. Badge and On Sale filter both work.
  • WooCommerce coupons never show a badge on the product page. Coupons apply at the cart level. Nothing about the coupon modifies a product’s get_sale_price() result on the storefront.
  • Runtime discount plugins (like Smart Cycle Discounts auto-apply campaigns) show a badge but skip the On Sale filter. They hook woocommerce_product_get_sale_price, so is_on_sale() returns true and the badge renders — but they don’t write to the database, so catalog-level on-sale queries can’t find them.
  • SCD code-gated campaigns behave like coupons. The discount is locked behind a code applied at cart. No product-page price change, no badge, until the code is entered.
  • Badge showing and On Sale filter finding are two separate things. The badge reads a per-product filter result at render time. The filter reads a database column. These can disagree — and for most discount plugins they will.
  • If the badge disappeared after enabling SCD Promotional Visuals, check the “Hide theme sale badge” setting. It suppresses the theme badge expecting a custom one. Without one assigned, the product goes badgeless.

Webstepper

WooCommerce tools for independent stores

We build Smart Cycle Discounts and TrustLens, and we write about running WooCommerce stores without the guesswork. Every feature claim in this post is verified against the current plugin source code.