We build custom Shopify features from $99. Every build has a 30-day guarantee.

Build My Feature →

Shopify Horizon: Show a Discount Code on the Product Page Without an App

Shopify Field Notes ··Johnny Chen Founder & Shopify Developer at CheckoutWorks
Cover titled Let Shoppers Copy Your Code in One Tap, with the code WELCOME10 shown in a bar at the top of a Shopify Horizon page and in a product page card with a check mark and a Copied label.

Shopify Horizon has no block that shows a discount code on the product page. You can add one with a single new block file and no edits to Horizon's own files. Shoppers click the code to copy it. The block sits inside Horizon's native Group block, so the card around it uses your theme's fonts, color schemes, borders, and spacing, and the code box has its own background, text, and border colors.

A second optional file adds a bar at the top of every page that holds the same block. We built and tested both on a Horizon 4.3.0 demo store, on desktop and at a 375px mobile width.

Shopify Horizon product page with a bordered card that reads Save 10% on your first order above the code WELCOME10 in a dashed box with a copy icon.

The card on our demo product: a native Group with a Text block and the new Discount code block inside.

To try it, open the live Horizon demo and click the code. The demo code is for testing the copy function and applies no discount.

What Horizon includes by default

Horizon 4.3.0 has one discount setting: Theme settings > Cart > Allow discounts in cart. It adds a field where shoppers type a code in the cart. Nothing in the theme shows a code to shoppers before they reach the cart.

Horizon does ship a small copy-to-clipboard component, used only on the gift card page. The block in this article reuses that component, so it adds no JavaScript of its own.

How the card is put together

The card is three blocks, and only one of them is new:

  • Group (native) is the card. It controls the background, color scheme, border, corner radius, padding, and layout.
  • Text (native) is the offer line, such as "Save 10% on your first order".
  • Discount code (new) is the code box. Clicking anywhere on it copies the code.

Keeping the new block this small has practical effects:

  • It follows your theme. The code uses your typography presets, and its default colors come from the color scheme around it. Change a font in Theme settings and the card changes with it.
  • You design the card with native blocks. Add an icon, a heading, a line of terms, or a divider inside the Group, and reorder them freely.
  • The code box has its own colors. Set a background, text, and border color on the block when the offer should stand out from the card.
  • It works wherever theme blocks are accepted. The block does not depend on the product, so the same code box can sit in a homepage section or a landing page.
  • It hides itself when the code is empty. That matters once the code comes from a product metafield, covered below.

Add the block file

Work on an unpublished duplicate of your theme. In Edit code, create blocks/cw-discount-code.liquid and paste the code below. The cw- prefix marks the file as custom.

{%- liquid
  assign cw_code = block.settings.code | strip
  assign cw_label = block.settings.button_label | default: 'Copy code'
-%}

{%- if cw_code != blank -%}
  <script
    src="{{ 'copy-to-clipboard.js' | asset_url }}"
    type="module"
    fetchpriority="high"
  ></script>
…
Show full codeCollapse code
{%- liquid
  assign cw_code = block.settings.code | strip
  assign cw_label = block.settings.button_label | default: 'Copy code'
-%}

{%- if cw_code != blank -%}
  <script
    src="{{ 'copy-to-clipboard.js' | asset_url }}"
    type="module"
    fetchpriority="high"
  ></script>

  <copy-to-clipboard-component
    class="cw-discount-code cw-discount-code--{{ block.settings.width }}"
    text-to-copy="{{ cw_code | escape }}"
    style="
      --cw-discount-code-border-style: {{ block.settings.border_style }};
      --cw-discount-code-radius: {{ block.settings.border_radius }}px;
      {% if block.settings.border_color != blank %}--cw-discount-code-border-color: {{ block.settings.border_color }};{% endif %}
      {% if block.settings.background_color != blank %}--cw-discount-code-background: {{ block.settings.background_color }};{% endif %}
      {% if block.settings.text_color != blank %}--cw-discount-code-color: {{ block.settings.text_color }};{% endif %}
    "
    {{ block.shopify_attributes }}
  >
    <button
      type="button"
      class="cw-discount-code__button"
      on:click="/copyToClipboard"
      aria-label="{{ cw_label | escape }}: {{ cw_code | escape }}"
      title="{{ cw_label | escape }}"
    >
      <span class="cw-discount-code__code {{ block.settings.type_preset | default: 'paragraph' }}">
        {{- cw_code | escape -}}
      </span>
      <svg class="cw-discount-code__icon cw-discount-code__icon--copy" aria-hidden="true" width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round">
        <rect x="5.5" y="5.5" width="8" height="8" rx="1.5"/>
        <path d="M10.5 3.5v-.25A1.25 1.25 0 0 0 9.25 2h-6A1.25 1.25 0 0 0 2 3.25v6a1.25 1.25 0 0 0 1.25 1.25h.25"/>
      </svg>
      <svg class="cw-discount-code__icon cw-discount-code__icon--done" aria-hidden="true" width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
        <path d="M3 8.5l3.25 3.25L13 4.75"/>
      </svg>
    </button>
    {%- if block.settings.success_message != blank -%}
      <span
        class="cw-discount-code__status visually-hidden"
        ref="copySuccessMessage"
        role="status"
      >
        {{ block.settings.success_message | escape }}
      </span>
    {%- endif -%}
  </copy-to-clipboard-component>
{%- elsif request.design_mode -%}
  <div class="cw-discount-code cw-discount-code--empty" {{ block.shopify_attributes }}>
    <p>Enter a discount code or connect a product metafield.</p>
  </div>
{%- endif -%}

{% stylesheet %}
  .cw-discount-code {
    box-sizing: border-box;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--gap-xs) var(--gap-sm);
    max-width: 100%;
    min-width: 0;
  }

  .cw-discount-code--fill {
    width: 100%;
  }

  .cw-discount-code--empty {
    padding: var(--padding-md);
    border: 1px dashed rgb(var(--color-foreground-rgb) / var(--opacity-30));
  }

  .cw-discount-code__button {
    box-sizing: border-box;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--gap-sm);
    max-width: 100%;
    margin: 0;
    padding: var(--padding-xs) var(--padding-md);
    appearance: none;
    font: inherit;
    text-align: start;
    color: var(--cw-discount-code-color, inherit);
    background: var(--cw-discount-code-background, transparent);
    border: 1px var(--cw-discount-code-border-style, dashed)
      var(--cw-discount-code-border-color, rgb(var(--color-foreground-rgb) / var(--opacity-30)));
    border-radius: var(--cw-discount-code-radius, 0);
    cursor: pointer;
  }

  .cw-discount-code--fill .cw-discount-code__button {
    flex: 1 1 auto;
  }

  .cw-discount-code__code {
    min-width: 0;
    margin: 0;
    color: inherit;
    overflow-wrap: break-word;
  }

  .cw-discount-code__icon {
    flex: none;
  }

  .cw-discount-code__icon--done,
  .cw-discount-code:has(.cw-discount-code__status:not(.visually-hidden)) .cw-discount-code__icon--copy {
    display: none;
  }

  .cw-discount-code:has(.cw-discount-code__status:not(.visually-hidden)) .cw-discount-code__icon--done {
    display: block;
  }

  .cw-discount-code__status {
    font-size: var(--font-size--sm);
  }
{% endstylesheet %}

{% schema %}
{
  "name": "Discount code",
  "tag": null,
  "settings": [
    {
      "type": "text",
      "id": "code",
      "label": "Discount code",
      "info": "Enter a code or connect a product metafield. The block is hidden when this is empty."
    },
    {
      "type": "select",
      "id": "type_preset",
      "label": "Code text preset",
      "options": [
        { "value": "paragraph", "label": "Paragraph" },
        { "value": "h1", "label": "Heading 1" },
        { "value": "h2", "label": "Heading 2" },
        { "value": "h3", "label": "Heading 3" },
        { "value": "h4", "label": "Heading 4" },
        { "value": "h5", "label": "Heading 5" },
        { "value": "h6", "label": "Heading 6" }
      ],
      "default": "h4",
      "info": "Presets are edited in Theme settings > Typography."
    },
    {
      "type": "select",
      "id": "width",
      "label": "Width",
      "options": [
        { "value": "fit", "label": "Fit content" },
        { "value": "fill", "label": "Fill" }
      ],
      "default": "fill"
    },
    { "type": "header", "content": "Appearance" },
    {
      "type": "color",
      "id": "background_color",
      "label": "Background color"
    },
    {
      "type": "color",
      "id": "text_color",
      "label": "Text color"
    },
    {
      "type": "select",
      "id": "border_style",
      "label": "Border",
      "options": [
        { "value": "none", "label": "None" },
        { "value": "dashed", "label": "Dashed" },
        { "value": "solid", "label": "Solid" }
      ],
      "default": "dashed"
    },
    {
      "type": "color",
      "id": "border_color",
      "label": "Border color",
      "visible_if": "{{ block.settings.border_style != 'none' }}"
    },
    {
      "type": "range",
      "id": "border_radius",
      "label": "Corner radius",
      "min": 0,
      "max": 40,
      "step": 1,
      "unit": "px",
      "default": 8
    },
    { "type": "header", "content": "Text" },
    {
      "type": "text",
      "id": "button_label",
      "label": "Accessible label",
      "default": "Copy code",
      "info": "Read by screen readers and shown on hover."
    },
    {
      "type": "text",
      "id": "success_message",
      "label": "Copied message",
      "default": "Code copied"
    }
  ],
  "presets": [
    {
      "name": "Discount code",
      "category": "Product"
    }
  ]
}
{% endschema %}

That is the whole installation for the product page. No other file changes.

Build the card in the theme editor

  1. Open the theme editor on a product page and select the product information section.
  2. Click Add block where the card should appear and choose Group. Below the price and above the variant picker is a common place.
  3. In the Group, set Direction to vertical, turn on a border, and add padding. We used a 1px solid border, an 8px corner radius, and 16px of padding on each side.
  4. Inside the Group, add a Text block and write the offer.
  5. Inside the Group, add the Discount code block. It is listed under Product. Enter the code in the Discount code field.

Shopify theme editor settings for the Discount code block, with the code field, code text preset, width, background color, text color, border, border color, and corner radius.

The block's settings. The Group around it holds the card's own background, border, and padding.

The block's settings:

  • Discount code: the code to show and copy.
  • Code text preset: which of your theme's typography presets the code uses.
  • Width: fill the available width, or fit the code.
  • Background color and Text color: leave them empty to follow the card.
  • Border, Border color, and Corner radius: a dashed, solid, or hidden outline.
  • Accessible label: what screen readers announce for the box, and the hover tooltip.
  • Copied message: the confirmation that appears after a click.

If you set a background color, set a text color that stays readable on it. The block does not adjust the contrast for you. To show the card on mobile only or desktop only, put it in a Responsive group.

Show a different code on each product

A code typed into the block appears on every product that uses the template. To vary it by product, store the code in a product metafield:

  1. In Settings > Metafields and metaobjects > Products, add a definition of type Single line text, for example custom.discount_code.
  2. Fill in the metafield on the products that have a code.
  3. In the block's Discount code field, click the connect icon and choose the metafield.

The connect icon sits at the top right of the field. Products with an empty metafield show no code box. The Group and Text blocks around it still render, so connect the Text block to a metafield as well, or keep the offer line general enough to make sense on its own.

Show the code at the top of every page

Horizon's announcement bar accepts only its own announcement block, so the Discount code block cannot go inside it. Two options cover that spot.

Without code: link the announcement to a discount link

Shopify gives every discount code a shareable link in the form /discount/YOURCODE. A shopper who opens it gets the discount applied at checkout automatically. Put that path in the announcement's Link setting and the whole announcement applies the code on click. There is no copy button, and none is needed.

With the copy box: add a promo bar section

To show the same click-to-copy box in the header area, create sections/cw-promo-bar.liquid and paste the code below. It is a plain bar that accepts any theme block.

{% render 'contrast-override', background_color: section.settings.background_color, section_id: section.id %}

<div class="section-background color-custom-{{ section.id }}"></div>
<aside
  class="cw-promo-bar spacing-style section section--{{ section.settings.section_width }} color-custom-{{ section.id }}"
  style="{% render 'spacing-padding', settings: section.settings %}"
>
  <div class="cw-promo-bar__content">
    {% content_for 'blocks' %}
  </div>
</aside>
…
Show full codeCollapse code
{% render 'contrast-override', background_color: section.settings.background_color, section_id: section.id %}

<div class="section-background color-custom-{{ section.id }}"></div>
<aside
  class="cw-promo-bar spacing-style section section--{{ section.settings.section_width }} color-custom-{{ section.id }}"
  style="{% render 'spacing-padding', settings: section.settings %}"
>
  <div class="cw-promo-bar__content">
    {% content_for 'blocks' %}
  </div>
</aside>

{% stylesheet %}
  .cw-promo-bar__content {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: var(--gap-xs) var(--gap-md);
    min-width: 0;
    text-align: center;
  }
{% endstylesheet %}

{% schema %}
{
  "name": "Promo bar",
  "blocks": [
    { "type": "@theme" },
    { "type": "@app" }
  ],
  "enabled_on": {
    "groups": ["header"]
  },
  "settings": [
    {
      "type": "select",
      "id": "section_width",
      "label": "Width",
      "options": [
        { "value": "page-width", "label": "Page" },
        { "value": "full-width", "label": "Full" }
      ],
      "default": "page-width"
    },
    {
      "type": "color",
      "id": "background_color",
      "label": "Background color"
    },
    { "type": "header", "content": "Padding" },
    { "type": "range", "id": "padding-block-start", "label": "Top", "min": 0, "max": 100, "step": 1, "unit": "px", "default": 8 },
    { "type": "range", "id": "padding-block-end", "label": "Bottom", "min": 0, "max": 100, "step": 1, "unit": "px", "default": 8 }
  ],
  "presets": [
    {
      "name": "Promo bar",
      "blocks": {
        "text": {
          "type": "text",
          "settings": {
            "text": "<p>Save 10% on your first order</p>",
            "width": "fit-content"
          }
        },
        "discount_code": {
          "type": "cw-discount-code",
          "settings": {
            "type_preset": "paragraph",
            "width": "fit"
          }
        }
      },
      "block_order": ["text", "discount_code"]
    }
  ]
}
{% endschema %}

In the theme editor, click Add section in the Header group and choose Promo bar. It starts with a Text block and a Discount code block. Enter the code, pick a background color for the bar, and drag it above or below the announcement bar. The bar picks a readable text color for the background you choose, using the same contrast logic as Horizon's own sections.

Dark bar above the Shopify Horizon header that reads Save 10% on your first order next to the code WELCOME10 in a dashed box with a copy icon.

The promo bar in the header group, above the store's header.

The same promo bar on a mobile screen, with the offer text on one line and the code box centered below it.

On mobile the code box moves below the text when the two no longer fit on one line.

Use it for Black Friday and other sales

A sale code works best when shoppers see it before they reach the cart. For a sale such as Black Friday and Cyber Monday, the two placements cover different jobs:

  • One code for the whole store: put it in the promo bar, so it shows on every page for the length of the sale.
  • Codes for selected products: use the product metafield, so only the products in the offer show a code box.

Add the block and test it well before the sale starts, and avoid theme changes during the sale itself. The block has no start or end date, so plan two manual steps: enter the code when the sale opens, and clear it when the sale ends. If the sale code has to combine with other discounts, test the combinations first. Our BFCM checklist covers that test and the other checks to run before the sale.

Check it before you publish

  • Copy: click the code on the storefront, then paste into the cart's discount field. The icon turns into a check mark and the confirmation appears next to the code.
  • Mobile: the code box stays on one line, and the offer text wraps above it when the two no longer fit.
  • The discount itself: place a test order or check the cart total. The block displays whatever text you enter, so a typo shows up here first.

Limits to know

  • It displays a code. It does not apply one. Shoppers still enter the code in the cart or at checkout. Keep Allow discounts in cart on if you want them to use it before checkout.
  • It does not know whether the code is valid. An expired or deleted discount keeps showing until you clear the field or the metafield. Add the card to your checklist when a promotion ends.
  • The confirmation stays visible after the first click until the page reloads. This is how Horizon's copy component behaves.
  • The confirmation does not prove the copy worked. By the code, the component shows the message without waiting for the browser's answer.
  • A click in the first moment after the page loads can be missed. The copy script runs once the page has finished loading, so on a slow connection a very early click does nothing. A second click works.
  • One code per block. Add a second block for a second code. Codes per variant are not supported.
  • The promo bar is separate from the announcement bar. It does not rotate with your announcements.

On page weight: the block loads one small script that already ships with Horizon, only on pages where the block has a code, and its styles live in the block file. It adds no app script and no request to another domain. We did not measure the page speed difference.

After a theme update

Both files are new, so a Horizon update leaves them in place, and they edit none of Horizon's files. They do rely on a few parts of the theme. Check them after an update:

  • assets/copy-to-clipboard.js and its copy-to-clipboard-component element. If an update renames or removes it, clicking stops copying.
  • The typography preset classes, paragraph and h1 to h6, and the visually-hidden class that hides the confirmation until the click.
  • For the promo bar: the contrast-override and spacing-padding snippets and the section classes it shares with Horizon's announcement bar.

A one-minute test covers them: open a product page, click the code, paste it, and look at the bar's colors. Our Horizon update workflow explains how to carry custom files into the new version.

Get Shopify help _

Want a discount code card?

We can add it to your theme.

$99 USD

ONE DISCOUNT CODE CARD ·30-DAY GUARANTEE

Add my discount code card