CheckoutWorks Shopify Partner

We fix Shopify bugs at a flat $49 per fix. Most fixes within 24 hours.

Fix My Bug →

Shopify Horizon: Add Custom Badges to Variant Buttons Without an App

·Johnny Chen Founder & Shopify Developer at CheckoutWorks
Cover titled Variant Button Badges Without an App, showing Badge Text values in the Shopify bulk editor above Horizon variant buttons labeled Start here, Save 19%, and Save 26%.

Shopify Horizon can show a short badge on each variant button, such as Start here or Save 19%, with a variant metafield and one small snippet. Every badge stays visible on the button’s top border, whether or not that option is selected. Buttons without badge text look the same as before.

We use this setup on our own Bug Fix service page, running Horizon 4.2.0. Below are the admin steps, the complete code, and the limits we kept on purpose.

Shopify Horizon variant buttons for 1 Fix, 3 Fixes, and 5 Fixes, with the badges Start here, Save 19%, and Save 26% on their top borders.

Each badge comes from the Badge Text metafield of its variant. The 1 Fix button is selected.

Check whether this setup fits your product

This version covers the most common case and stays small enough to maintain:

  • Products with one option. When a product has one option, such as Size or Quantity, each option value belongs to exactly one variant, so each button has one clear badge. When a product has two or more options, one value such as Small belongs to several variants. The snippet shows no badges on those products.
  • Button-style variant pickers. Swatches and dropdowns stay unchanged. The last section explains what they would need.
  • Text you control. The badge shows the text you enter. It does not calculate discounts, so you update any savings text when prices change.

Create the badge text metafield

The badge text lives in a variant metafield, so you edit it in Shopify admin without touching code again.

  1. In Shopify admin, go to Settings > Metafields and metaobjects > Variants and add a definition.
  2. Name it Badge Text. Make sure the namespace and key are custom.badge_text, because the snippet reads that exact key.
  3. Choose Single line text and keep it as one value.
  4. Optional: add a maximum character count, such as 16. Short badges keep the buttons tidy on mobile.

Shopify variant metafield definition named Badge Text with the key custom.badge_text and the type Single line text.

Add badge text to each variant

Open a product and edit each variant, or use the bulk editor when you have many variants. Add the Badge Text column and fill in only the variants that need a badge. Leave the field empty for no badge.

Shopify bulk editor showing three variants with the Badge Text values Start here, Save 19%, and Save 26%.

A few tips for the text:

  • Type it in normal case. The theme shows it in uppercase.
  • Keep it to two or three words. Long badges can crowd narrow mobile buttons.
  • Calculate savings from the compare-at price and round down, so the badge never overstates the discount. For example, $119 against a $147 compare-at price saves 19.05%, so we use Save 19%.
  • Claims such as Most popular should be true for your store. A neutral label such as Start here works when you have no sales data to support a ranking.

Add the badge snippet

Work on an unpublished duplicate of your theme. In the code editor, create snippets/variant-option-badge.liquid and paste the code below.

{%- liquid
  assign badge_text = blank
  if product_resource.options.size == 1
    assign badge_text = product_option_value.variant.metafields.custom.badge_text.value | strip
  endif
  assign badge_id = 'VariantBadge-' | append: block_id | append: '-' | append: product_option_value.id
-%}

{%- if badge_text != blank -%}
  {%- if attribute -%}
…
Show full codeCollapse code
{%- liquid
  assign badge_text = blank
  if product_resource.options.size == 1
    assign badge_text = product_option_value.variant.metafields.custom.badge_text.value | strip
  endif
  assign badge_id = 'VariantBadge-' | append: block_id | append: '-' | append: product_option_value.id
-%}

{%- if badge_text != blank -%}
  {%- if attribute -%}
    aria-describedby="{{ badge_id }}"
  {%- else -%}
    <span
      id="{{ badge_id }}"
      class="variant-option__badge"
    >
      {{- badge_text | escape -}}
    </span>
  {%- endif -%}
{%- endif -%}

{% stylesheet %}
  /*
    The badge straddles the top border, so buttons in a badge row cannot clip overflow.
    Horizon's sliding selected pill relies on that clipping, so these rows use a static
    selected background instead. Recheck these overrides after theme updates.
  */
  .variant-option--buttons:has(.variant-option__badge) {
    --variant-badge-offset: 0.8em;

    padding-block-start: var(--variant-badge-offset);
    row-gap: calc(var(--gap-sm) + var(--variant-badge-offset));

    .variant-option__button-label {
      overflow: visible;
      padding-block: 1em;
    }

    .variant-option__button-label__text {
      font-size: var(--font-size--lg);
    }

    .variant-option__button-label__pill {
      display: none;
    }

    .variant-option__button-label:has(:checked):not(:has([data-option-available='false'])) {
      background-color: var(--color-selected-variant-background);

      &:hover {
        background-color: var(--color-selected-variant-hover-background);
      }
    }
  }

  .variant-option__badge {
    position: absolute;
    inset-block-start: 0;
    left: 50%;
    z-index: 3;
    pointer-events: none;
    padding: 0.35em 0.7em;
    border-radius: var(--style-border-radius-xs);
    background-color: var(--color-selected-variant-border);
    color: var(--color-variant-background);
    font-family: var(--font-subheading--family);
    font-weight: var(--font-subheading--weight);
    font-size: var(--font-size--2xs);
    letter-spacing: 0.02em;
    text-transform: uppercase;
    line-height: 1.2;
    transform: translate(-50%, -50%);
  }
{% endstylesheet %}

The snippet does three things:

  • It reads custom.badge_text only when the product has exactly one option, and it outputs nothing when the field is empty.
  • It outputs either the visible badge or an aria-describedby attribute, depending on where you render it. The next step uses both.
  • It styles the badge with Horizon’s own color, font, and size variables, so the badge follows your color scheme. The CSS applies only to button rows that contain a badge.

Render the badge in the variant picker

Open snippets/variant-main-picker.liquid. Inside the loop that renders the variant buttons, find the radio <input>. Add the first block just before the input’s closing >:

{% unless variant_style == 'swatch' %}
  {% render 'variant-option-badge',
    product_resource: product_resource,
    product_option_value: product_option_value,
    block_id: block.id,
    attribute: true
  %}
{% endunless %}

Then find the variant-option__button-label__text span that prints the option name, and add the second block right after it:

{% render 'variant-option-badge',
  product_resource: product_resource,
  product_option_value: product_option_value,
  block_id: block.id
%}

The first block matters for accessibility. Horizon gives each radio input an aria-label with only the option name, so screen readers skip other text inside the button. The aria-describedby attribute adds the badge text as a description.

These two blocks are the only changes to Horizon’s own files. Everything else lives in the new snippet.

Test the result

Preview the duplicate theme and check a product that has badge text. On our store, we checked these states:

  • Every badge shows on desktop and at a 375px mobile width.
  • After you switch to another variant, the badges stay in place and the selected style moves to the new button.
  • Each radio input with a badge has an aria-describedby that points to its badge.
  • Shopify Theme Check reports no issues in the changed files.

The same Shopify Horizon variant buttons with 3 Fixes selected and all three badges still visible.

After you switch to 3 Fixes, every badge stays in place.

Shopify Horizon variant buttons with badges at a 375px mobile width.

At 375px wide, the badges fit above each button.

Also check a product without badge text and a product with two or more options. Both should look exactly as they did before. Horizon uses the same variant picker in other places, such as quick add, so check those if your theme uses them. We did not test quick add for this article.

Recheck the badges after a Horizon update

When you update Horizon, copy snippets/variant-option-badge.liquid to the new version as it is. Then add the two render blocks to the new variant-main-picker.liquid instead of copying the old file over it. Our guide to updating Shopify Horizon without losing custom code covers that process.

The badge sits partly outside the button, so the CSS changes two Horizon behaviors in rows that contain a badge:

  • Horizon clips anything outside the button with overflow: clip. The snippet sets overflow: visible so the badge is not cut off.
  • Horizon animates the selected background as a sliding element with the class variant-option__button-label__pill, and that animation depends on the clipping. The snippet hides it and uses a static selected background instead.

After an update, confirm that these class names still exist and that the badge is not clipped. If Horizon changes this markup, adjust the CSS rather than restoring an old theme file.

Swatches, dropdowns, and products with several options

We left these cases out because each one needs its own design decision:

  • Swatches: the swatch button is the color sample itself, so a badge in the same place would cover it. Swatches need a different layout, such as a badge below the swatch.
  • Dropdowns: a native dropdown option can hold only plain text. You can add the badge text to the option name, but you cannot style it as a badge.
  • Several options: you first need a rule for which badge a shared value such as Small should show, for example only after the shopper selects the other options.

If your store needs one of these versions, we can build it through our Shopify Feature Implementation service, which covers one focused custom feature like this.

Get Shopify help _

Want badges on your variant buttons?

We can add them to your theme.

$99 USD

ONE CUSTOM FEATURE · 30-DAY GUARANTEE

Add my variant badges