Replace Sale Badges with Savings Percentages on Shopify (Liquid Code)

The short answer: Replace generic “Sale” badges with dynamic savings percentages using this Liquid code: {% assign savings_pct = product.compare_at_price | minus: product.price | times: 100 | divided_by: product.compare_at_price %} then output -{{ savings_pct }}% in the badge. Add a minimum threshold (10%+) to avoid showing trivial discounts. No apps needed, no JavaScript, works on every Shopify theme.


If every product in your store shows a “Sale” badge, none of them do.

I see this on almost every Shopify store I audit. The theme shows a generic “Sale” label on any product where compare_at_price is set. Since most merchants set compare-at prices on everything, the result is a wall of identical red badges that communicate nothing.

A store I recently audited had “Salg” (Norwegian for “Sale”) on 100% of product cards. Every single card. The badge was supposed to signal a deal but instead it was visual noise that shoppers had trained themselves to ignore.

The fix takes 5 minutes: replace the generic text badge with a calculated savings percentage. Instead of “Sale”, show “-42%”. Instead of a label, show a number.

Why Percentages Convert Better Than Labels

Specificity creates urgency. “-42%” tells the customer exactly what they save. “Sale” tells them nothing they did not already assume.

Price anchoring. When a shopper sees “-42%”, their brain automatically anchors to the original price and perceives the current price as a deal. Generic “Sale” labels do not trigger this anchoring effect because there is no number to anchor to.

Differentiation across your catalog. When some products show “-15%” and others show “-50%”, shoppers can quickly identify the best deals. When everything says “Sale”, there is no way to distinguish a 10% markdown from a 60% clearance.

Reduced badge blindness. Shoppers who have learned to ignore “Sale” badges will notice a percentage because it is different and carries real information.

The Liquid Code

Find your theme’s product card snippet. In most themes, this is one of:

  • snippets/card-product.liquid (Dawn and OS 2.0 themes)
  • snippets/product-card.liquid
  • snippets/product-card-grid.liquid

Search for the existing sale badge code. It usually looks something like this:

{% if product.compare_at_price > product.price %}
  <span class="badge badge--sale">Sale</span>
{% endif %}

Replace it with:

{% if product.compare_at_price > product.price %}
  {% assign savings = product.compare_at_price | minus: product.price %}
  {% assign savings_pct = savings | times: 100 | divided_by: product.compare_at_price %}
  <span class="badge badge--sale">-{{ savings_pct }}%</span>
{% endif %}

That is the entire change. Three lines of Liquid math, same badge markup.

How the Math Works

The calculation is straightforward:

  1. savings = compare_at_price minus current price (the absolute discount amount)
  2. savings_pct = savings divided by compare_at_price, times 100 (the percentage)

For a product originally priced at 1,000 and currently 580:

savings = 1000 - 580 = 420
savings_pct = 420 * 100 / 1000 = 42

Badge shows: -42%

Liquid does integer division by default, so the result is always a whole number. No decimals, no rounding issues. A 33.7% discount displays as “-33%”, which is fine for badge purposes.

Adding a Minimum Threshold

Small discounts can look unimpressive. A “-3%” badge might actually hurt perceived value by making the discount seem trivial. Add a minimum threshold:

{% if product.compare_at_price > product.price %}
  {% assign savings = product.compare_at_price | minus: product.price %}
  {% assign savings_pct = savings | times: 100 | divided_by: product.compare_at_price %}
  {% if savings_pct >= 10 %}
    <span class="badge badge--sale">-{{ savings_pct }}%</span>
  {% endif %}
{% endif %}

Now only products with 10% or more off show a badge. Adjust the threshold to whatever makes sense for your pricing strategy.

Showing Both Percentage and Amount

For high-ticket products, showing the absolute savings alongside the percentage can be more persuasive. “$200 off” hits harder than “-15%” on a $1,300 product.

{% if product.compare_at_price > product.price %}
  {% assign savings = product.compare_at_price | minus: product.price %}
  {% assign savings_pct = savings | times: 100 | divided_by: product.compare_at_price %}
  {% if savings_pct >= 10 %}
    <span class="badge badge--sale">
      -{{ savings_pct }}% (Save {{ savings | money }})
    </span>
  {% endif %}
{% endif %}

The | money filter formats the savings amount using your store’s currency settings. Always use | money instead of hardcoding currency symbols.

Styling the Badge

Most themes already style .badge--sale with a red or accent background. The percentage text is typically shorter than “Sale” so the badge may shrink slightly. If you want to ensure consistent sizing:

.badge--sale {
  min-width: 3rem;
  text-align: center;
}

For a more modern look, consider using your theme’s accent color instead of the default red. A dark badge with white text often performs better than an aggressive red:

.badge--sale {
  background-color: var(--color-badge-sale, #1a1a2e);
  color: #fff;
  font-weight: 600;
  font-size: 0.75rem;
  padding: 0.25rem 0.5rem;
  border-radius: 4px;
  min-width: 3rem;
  text-align: center;
}

Handling Collection Page Badges vs PDP Badges

The code above works on collection page product cards. For the product detail page (PDP), the badge logic is usually in a different file:

  • sections/main-product.liquid (Dawn)
  • snippets/product-price.liquid
  • snippets/price.liquid

Apply the same replacement. Search for the existing sale badge or “Sale” text and swap in the percentage calculation. The Liquid is identical. Only the file location differs.

What About Variant-Specific Discounts?

If your products have variants with different compare-at prices (e.g., Small is 20% off but Large is 30% off), the default product.compare_at_price uses the first available variant.

For variant-aware badges on the PDP:

{% assign current_variant = product.selected_or_first_available_variant %}
{% if current_variant.compare_at_price > current_variant.price %}
  {% assign savings = current_variant.compare_at_price | minus: current_variant.price %}
  {% assign savings_pct = savings | times: 100 | divided_by: current_variant.compare_at_price %}
  <span class="badge badge--sale">-{{ savings_pct }}%</span>
{% endif %}

On collection pages, variant-specific badges are not practical because only one variant displays per card. The default product.compare_at_price behavior is correct for that context.

Common Mistakes

Using product.price_max instead of product.price. price_max returns the highest variant price, not the current or first-available variant price. This produces incorrect savings percentages.

Forgetting the | money filter on savings amounts. If you show “Save 4200” instead of “Save $42.00”, the number is in cents (Shopify stores prices in cents internally). Always pipe through | money when displaying currency values.

Hardcoding currency symbols. Never write ${{ savings | divided_by: 100 }}. Use {{ savings | money }}. This ensures correct formatting across currencies, locales, and Shopify Markets multi-currency setups.

Showing negative percentages on products without compare-at prices. The {% if product.compare_at_price > product.price %} guard handles this, but make sure you keep it. Without the conditional, products without a compare-at price would show “-0%” or cause a division-by-zero error.

Before and After

Before: Every card shows “Sale”. Shoppers cannot tell which products have the best discounts. The badge adds no information and trains shoppers to ignore it.

After: Cards show “-15%”, “-30%”, “-50%”. Shoppers immediately see which products have the deepest discounts. Products below your threshold show no badge at all, making the ones that do show a badge more meaningful.

One small Liquid change. Zero app fees. Zero JavaScript. Immediate impact on how shoppers perceive your pricing.

For more Liquid snippets that replace paid apps, see 15 Shopify Liquid Snippets That Replace Expensive Apps. For the full CRO audit framework that catches issues like this, read the Shopify CRO Audit Checklist.

Frequently Asked Questions

Why are Sale badges bad for conversions?

When every product on your store shows a Sale badge, the badge becomes meaningless. Shoppers learn to ignore it because it communicates nothing specific. A savings percentage like -42% tells the customer exactly how much they save, which creates urgency and anchors the discount to a real number. Specificity converts better than generic labels.

Does this work with Shopify OS 2.0 themes?

Yes. The Liquid code uses compare_at_price and price, which are core Shopify Liquid properties available on every theme since Shopify's earliest versions. The only part you may need to adjust is the CSS class names to match your specific theme's card markup.

What if a product has variants with different prices?

The code in this guide uses product.compare_at_price and product.price, which return the values for the currently selected or first available variant. For products where variants have significantly different discounts, you can use product.selected_or_first_available_variant.compare_at_price for more precision. Most themes handle this correctly by default.

Will this affect my page speed?

No. This is pure Liquid, which renders server-side before the page is sent to the browser. There is no JavaScript involved, no external API calls, and no additional HTTP requests. The savings percentage is calculated and baked into the HTML during Shopify's server render. It is actually faster than app-based badge solutions that inject badges via JavaScript after page load.

How do I show the badge only when the discount is above a minimum threshold?

Add a minimum check after the percentage calculation. For example, to only show badges for discounts of 10% or more, wrap the badge HTML in a conditional: {% if savings_pct >= 10 %}. This prevents tiny 3-5% discounts from displaying a badge, which can look unimpressive and actually hurt perceived value.