Product Badges in WooCommerce
WooCommerce already outputs a sale badge and gives you a filter to change it. Most badge plugins are a settings screen over the same two template hooks you could use directly.
WooCommerce ships one badge and one filter, and the filter is more capable than most people realise. The sale flash is printed on the catalogue and on the product page, its markup passes through a filter before output, and custom badges attach to the same template hooks the thumbnail uses. Before buying a badge plugin, be clear that what you are buying is a settings screen, some preset conditions and a design picker — not a capability that WooCommerce lacks.
What core does already
On archive listings, WooCommerce prints the sale badge inside the loop item, before the title and alongside the thumbnail. On the product page it prints it before the image gallery. Both output a small element with the class onsale, and both pass that markup through the woocommerce_sale_flash filter, which receives the markup, the post and the product object. Returning different markup from that filter changes every sale badge on the site in one place.
Whether a product counts as on sale is decided by WooCommerce, not by you: a sale price must be set, be lower than the regular price, and fall within its scheduled dates if any are set. For a variable product, the badge appears when any variation qualifies. That is worth knowing before you promise a customer that the badge means the price they see is discounted — if you are still structuring product variations, the badge behaviour follows from how you set prices at variation level.
Adding your own badges
Custom badges are conditional markup printed into the loop item. The hooks are the ones the thumbnail already uses: the badge belongs alongside the image so it can be positioned over it. The pattern is always the same three parts:
- A condition read from the product object — publish date for "new", stock quantity for "low stock", a taxonomy term or custom field for anything editorial.
- A small piece of markup with a class you control, printed on the appropriate hook.
- CSS that positions it absolutely inside the image wrapper, which must itself be positioned. The badge also needs to sit above the product link's own stacking context, or it will be visible but unclickable and, worse, will swallow clicks meant for the card.
That last point is where hand-built badges usually go wrong. The rest is a few lines of custom CSS and a small snippet.
The badges people actually want
| Badge | Data it needs | Difficulty |
|---|---|---|
| Sale | Native | None |
| Percentage off | Regular and sale price | Easy for simple products |
| Percentage off, variable product | Prices across all variations | Genuinely awkward |
| New | Publish date and a threshold | Easy |
| Low stock | Stock quantity, stock management enabled | Easy, but caching-sensitive |
| Out of stock | Stock status | Easy |
| Best seller | Accumulated sales count | Easy to read, expensive to sort by |
| Editorial, such as "staff pick" | A taxonomy or custom field | Easy, and the most useful |
The variable product row is the one that catches people out. There is no single discount for a product whose variations are discounted differently, so you have to decide what the badge means — the largest discount available, the smallest, or nothing at all — and say so in the label. "Up to 40% off" is honest; a bare "40% off" on a product where one variation is discounted is not.
Editorial badges are underrated. A taxonomy a shop manager can tick does more for merchandising than any automatic condition, and it needs no plugin.
Where badges break
Caching. Badges derived from stock or sales are computed when the page is generated and then frozen into whatever cache holds it. "Only 2 left" is embarrassing when it is three days old. Either keep stock-derived badges off cached pages or accept the staleness deliberately, and understand which pages your cache is holding.
Duplication. Themes frequently print their own sale badge as well as, or instead of, the core one. Adding a plugin then gives you two. If a badge appears twice, look for a theme template override before assuming a bug.
Layout. Badges sit over images, so they inherit every image problem. A badge positioned for a square thumbnail lands in the wrong place on an uncropped one, which is one more reason to settle your product image sizes before designing the card. How the badge relates to the rest of the card is part of the same decision as the product card design.
Truthfulness. Consumer pricing rules in many markets require a reference price to be a price genuinely charged recently. Badges that manufacture a discount by inflating the regular price are a legal exposure, not a merchandising tactic.
What to install, if anything
If you want the sale badge to say something else, that is a filter. If you want two or three conditional labels, that is a snippet and some CSS. Buy a plugin when the store's merchandising team needs to create and schedule badges themselves without a developer, because that workflow — rules, scheduling, a visual editor — is the real product. Everything below it, core already does.
Frequently asked
- The badge markup passes through the `woocommerce_sale_flash` filter before it is printed, so a short filter returns whatever markup you want in its place. Overriding the sale flash template files achieves the same thing if you already maintain template overrides.
- WooCommerce shows it only when a product is genuinely on sale, meaning a sale price is set and lower than the regular price, and any scheduled dates are current. For variable products it appears when at least one variation is on sale, which is why a badge can appear on a product whose displayed price has not changed.
- Because the archive page is being served from a cache created when the stock level was different. Anything computed per request and printed into cached HTML will go stale, so stock-sensitive badges need to be excluded from full-page caching or loaded separately.