[Horizon] Customizing Product Card Badge Display

[Horizon] Customizing Product Card Badge Display — From File Tracing to Code Changes

"I want to display discount rates on product card badges in the featured collection"

We sometimes receive requests to display specific discount rates or amounts like "20% OFF" on sale badges, rather than just "SALE", to better catch customers' attention. Unfortunately, Horizon does not include this feature as standard.

In this article, we'll walk through the implementation process, starting from the actual file investigation.

With the Dawn/Rise theme, the featured collection section simply loads snippets/card-product.liquid , so you can reach the target file in just 1 step.

With the Horizon theme, while the actual files to modify are few, you need to read through multiple files just to locate them. We'll walk through the file-tracing process unique to Horizon's block architecture and the specific customization approach step by step.

Note: In this article, we intentionally do not create custom.* files as recommended by Horizon's customization guidelines, and instead directly edit vendor files. The reasons for this are explained in detail, so please review that section as well.

Final Result

A before-and-after comparison. We modify the SALE badge text to display discount rates or amounts instead of just "Sale". Note that sale price display (strikethrough original price alongside the discounted price) is already a standard Horizon feature, so only the badge portion needs modification.

Horizonテーマ 割引額バッジ's表示例

How to Find the Target File in Dawn

First, let's check how this is handled in the Dawn/Rise theme for comparison.

In Dawn/Rise, opening the featured collection section sections/featured-collection.liquid reveals that the product card rendering is handled by snippets/card-product.liquid , which is called directly. Opening this snippet reveals images, badges, product names, and prices — all contained in a single file, so modifying the badge simply requires changing a few lines of Liquid code.

// Dawn/Rise approach

sections/featured-collection.liquid    ← Section
  └─ snippets/card-product.liquid        ← Images, badges, titles, prices — all here

From section to snippet in 1 step. The scope of impact is contained within that single file — Dawn's design philosophy of "giving a single snippet full responsibility for the product card" directly contributes to ease of customization.

Product Cards in Horizon: A Chain of Files

In Horizon, the product card is implemented as a nested structure of "theme blocks". Understanding how a single product card renders requires following this chain of files:

// Homepage "Featured Collection" → Product card file chain

sections/product-list.liquid             ← Section本体
  └─ blocks/_product-card.liquid            ← Product card block (placed as static block)
       └─ snippets/product-card.liquid       ← Card shell (Web Component)
            ├─ blocks/_product-card-gallery.liquid  ← Badge is here
            │    └─ snippets/card-gallery.liquid       ← Image slideshow
            ├─ blocks/product-title.liquid           ← Product name
            └─ blocks/price.liquid                   ← Price (sale display built-in)
                 └─ snippets/price.liquid             ← Price formatting

Dawn's card-product.liquid was contained in a single file, but in Horizon it is distributed across multiple files.

Why is it structured this way?

Horizon adopts a "Theme Block Architecture" where each element is made independent at the block level, enabling flexible reordering and visibility toggling in the theme editor. Since images, titles, and prices are each independent blocks, they can be reordered via drag-and-drop or hidden from the theme editor. As a side effect of this flexibility, directly editing the code requires tracing through multiple files.

File Tracing in Practice

Let's trace through the actual steps needed to modify the "SALE badge" text.

Step 1: Open the section → Confirm delegation to blocks

First, we look for the section responsible for the "Featured Collection" on the homepage.templates/index.json reveals that the relevant section is sections/product-list.liquid . Opening it shows that product card rendering is delegated via {% content_for 'block', type: '_product-card' %} . The product card content is defined on the block side.

Step 2: _product-card.liquid → Delegates to snippet

blocks/_product-card.liquid contains only 3 lines of code:

{% capture children %}
  {% content_for 'blocks', closest.product: product %}
{% endcapture %}

{% render 'product-card', children: children, product: product %}

It gathers the output of child blocks (image, title, price, etc.) into the children variable and passes it to theproduct-card snippet. Since the badge code is in the child blocks, we proceed to the next step.

Step 3: product-card.liquid → Card shell only

snippets/product-card.liquid defines the Web Component <product-card> tag shell. The content is injected via {{ children }} . The badge code is contained within this children .

We've traced through 3 files so far. Now, {{ children }} — to find out what goes in here, we need to check a different part of the section file.

Step 4: Return to product-list.liquid → Child block structure defined in presets

{{ children }} contains can be found by returning to the original section file sections/product-list.liquid . The schema's presets defines the product card's child block structure.

Let's first understand the overall preset structure. This section has 3 presets defined, corresponding to layout variations available when adding sections in the theme editor.

// product-list.liquid schema presets overview

"presets": [
  { "name": "products_grid",      ... },   ← Grid layout
  { "name": "products_carousel",  ... },   ← Carousel layout
  { "name": "products_editorial", ... }    ← Editorial layout
]

Looking inside each preset, you'll findsettings (layout settings) and blocks (block structure) defined.products_grid as an example, the blocks defined are static-header (section header) and static-product-card (product card).

// products_grid preset structure

{
  "name": "products_grid",
  "settings": {
    "layout_type": "grid",
    "columns": 4,
    "max_products": 8,
    ...
  },
  "blocks": {

    // ① Section header (title + View All button)
    "static-header": {
      "type": "_product-list-content",
      "blocks": {
        "product_list_text":   { "type": "_product-list-text" },    ← Collection title
        "product_list_button": { "type": "_product-list-button" }   ← "View all" button
      }
    },

    // ② Product card (applied repeatedly for each product)
    "static-product-card": {
      "type": "_product-card",
      "blocks": {
        "product-card-gallery": { "type": "_product-card-gallery" }, ← Product image + Badge
        "product_title":        { "type": "product-title" },        ← Product title
        "price":                { "type": "price" }                 ← Price
      },
      "block_order": ["product-card-gallery", "product_title", "price"]
    }
  }
}

Mapping this structure to the on-screen layout looks like this:

プリセット構造と画面レイアウト's対応

Now we know what {{ children }} contains._product-card-gallery (product image + badge),product-title (product title), andprice (price) — these 3 blocks are displayed in this order. The badge modification target is _product-card-gallery .

All 3 preset types share the same block structure, with only the settings values (column count, spacing, navigation style, etc.) differing between grid, carousel, and editorial. Therefore, the badge modification applies to sections added with any preset.

Multiple presets can be defined

Incidentally, product-list.liquid 's presets には3つ'sパターンが定義されています。products_grid (grid layout),products_carousel (carousel layout), andproducts_editorial(エディトリアル表示)です。1つ'sセクションFileに複数'spresetsを定義できる'sがShopifyテーマ's特徴で、テーマエディタ'sセクション追加画面で「特集コレクション:グリッド」「特集コレクション:カルーセル」「特集コレクション:エディトリアル」'sように、In the sameセクションを異なる初期設定で追加できる仕組みです。レイアウトは違っても、中's商品カードブロック構成は共通な'sで、今回'sバッジ変更はすべて'sパターンに反映されます。

Step 5:_product-card-gallery.liquid → バッジ'sコードを発見

Opening blocks/_product-card-gallery.liquid を開くと、バッジ'sコードが見つかります。

{%- if product.available == false or product.compare_at_price > product.price -%}
  <div class="product-badges__badge product-badges__badge--rectangle ...">
    {%- if product.available == false -%}
      {{ 'content.product_badge_sold_out' | t }}
    {%- elsif product.compare_at_price > product.price -%}
      {{ 'content.product_badge_sale' | t }}   ← ここが「SALE」's文字列
    {%- endif -%}
  </div>
{%- endif -%}

5File目で、変更すべき箇所に到達しました。Step 1'sセクションに一度戻ってpresetsを確認するという流れが、Horizonならでは's特徴的なポイントです。

実際'sChange Description

File追跡's結果、変更する'sは blocks/_product-card-gallery.liquid .

ここで重要な'sは、元'sコードを直接書き換える'sではなく、schemaに設定項目を追加してテーマエディタから切り替えられるようにすることです。こうすることで、元's「セール」テキスト表示がデフォルト値として維持され、コードを触らずに元に戻すこともできます。テーマアップデート時'sマージでも、元コード's削除より設定's追加's方が競合しにくくなります。

Change 1: Add setting to schema

_product-card-gallery.liquid 'sschema(settings配列)に、バッジ表示形式's選択肢を追加します。

// schema 's settings 配列に追加
{
  "type": "select",
  "id": "sale_badge_style",
  "label": "セールバッジ's表示形式",
  "options": [
    { "value": "text",     "label": "Text (Sale)" },
    { "value": "percent",  "label": "Discount rate (20% OFF)" },
    { "value": "amount",   "label": "Discount amount (¥1,000 OFF)" }
  ],
  "default": "text"
}

default"text" にすることで、何もしなければ元's動作(「セール」テキスト)がそ'sまま維持されます。

Change 2: Branch Liquid template based on setting value

In the same _product-card-gallery.liquid 'sLiquidテンプレート部分(schema より上'sコード)で、変更1で追加した sale_badge_style 's値に応じてバッジ'sテキスト表示を切り替えます。

Before:

{%- elsif product.compare_at_price > product.price -%}
  {{ 'content.product_badge_sale' | t }}

After:

{%- elsif product.compare_at_price > product.price -%}
  {%- case block.settings.sale_badge_style -%}
    {%- when 'percent' -%}
      {%- assign discount = product.compare_at_price
          | minus: product.price
          | times: 100.0
          | divided_by: product.compare_at_price
          | round -%}
      {{ discount }}% OFF
    {%- when 'amount' -%}
      {%- assign save_amount = product.compare_at_price
          | minus: product.price -%}
      {{ save_amount | money }} OFF
    {%- else -%}
      {{ 'content.product_badge_sale' | t }}
  {%- endcase -%}

else に元's翻訳キー表示を残している'sで、デフォルト("text")'sままなら動作は一切変わりません。テーマエディタ's「商品画像」ブロック設定パネルから、「Text (Sale)」「Discount rate (20% OFF)」「Discount amount (¥1,000 OFF)」を選択するだけで切り替えられます。

テーマエディタで'sセールバッジ表示形式's設定切り替え画面

なぜ設定項目にする'sか

元'sコードを直接書き換えてしまうと、元に戻すにはコードを再度編集する必要があります。schema設定として追加すれば、テーマエディタ'sドロップダウンから誰でも切り替えられ、デフォルト値で元's動作が保たれます。テーマアップデート時'sマージでも、コード's削除や書き換えより、設定's追加's方が競合が起きにくいというメリットもあります。

変更File's全体像

File Type Change Description
blocks/_product-card-gallery.liquid ベンダーFile変更 SALEバッジ'sテキストを割引率に変更

加えて、これら's変更を行うために読み込んで理解する必要があったFileは以下's通りです。

File Why understanding is needed
sections/product-list.liquid 起点。商品カードブロックへ's委任を確認
blocks/_product-card.liquid 子ブロック's集約とスニペットへ's受け渡しを確認
snippets/product-card.liquid Understand Web Component and DOM structure
snippets/card-gallery.liquid 画像とバッジ's位置関係、スライドショー構造を把握

変更するFileは1つだけですが、そこにたどり着くまでに合計5Fileを読み解くこと.Dawn's「セクション → スニペット」's1ステップと比べると、File追跡's手順に大きな違いがあることがわかります。

「ベンダーFileを触らない」方針と、触らざるを得ない現実

以前's記事 recommends completing customizations using onlycustom.* プレフィックス付き'sFileだけ — self-containedさせ、ベンダーFileを変更しないことを推奨しています。CSS(assets/custom.css) and JS (assets/custom.js), and new sections (sections/custom.*.liquid)を追加する分にはこ's方針に沿えます。

しかし今回'sように既存ブロック's動作を変更するカスタマイズでは、こ's方針に沿うことが現実的に成り立ちません。

What happens if you try the custom.* approach?

Following the guidelines, instead of directly editing_product-card-gallery.liquid , you would createcustom._product-card-gallery.liquid — Create newしてベンダーFileを温存するはずです。しかし、こ'sカスタムブロックを実際に使えるようにするには、参照元'sFileを全て書き換える必要があります

Let's count the specifics.

Step 1:親ブロック'sschemaに新typeを追加(2File)

_product-card-gallery を子ブロックとして許可している'sは、プライベートブロック _product-card.liquid as a child block are the private block product-card.liquid 's2Fileです。それぞれ'sschema'sblocks配列に custom._product-card-gallery を追加する必要があります。→ ベンダーFile2つを変更

Step 2:presets'sデフォルト構成を書き換え(3File)

各セクション'sschema内presetsで "type": "_product-card-gallery" need to be rewritten to the new type.product-list.liquid (3 presets),product-recommendations.liquid(セクション・ブロック)。→ ベンダーFile3つを変更

Step 3:テンプレートJSON's実行時構成を書き換え(6File)

テーマエディタで配置済み's構成が保存されているテンプレートJSON(index.json, collection.json, product.json, search.json, cart.json, 404.json)も全て書き換えが必要です。→ ベンダーFile6つを変更

Approach 変更するベンダーFile数
_product-card-gallery.liquid — Direct edit 1File
custom._product-card-gallery.liquid — Create new 11File (parent schemas 2 + presets 3 + template JSONs 6)

1File'sベンダー変更を避けるために、11File'sベンダー変更が必要.

customApproach's変更連鎖図

これはHorizon'sブロックアーキテクチャに起因する構造的な特性です。ブロックFileはschema'sblocks配列で「許可するタイプ」として参照され、presetsとテンプレートJSONで「実際に使うタイプ」として指定されています。ブロック'sタイプ名を変えるということは、そ's参照元を全て書き換えることを意味します。

さらにcustomApproachを徹底すると

もし、参照元'sFileに変更をかけること自体もcustomApproachで対応するとすれば、変更はさらに拡大します。Step 1で _product-card.liquidcustom._product-card.liquid に置き換えるなら、今度はそ's親である product-list.liquid 'sschemaやpresetsも書き換えが必要.そこもcustom化するなら、テンプレートJSONも書き換え...という形で、変更が再帰的に上位レイヤーへ波及していきます。最終的には、テーマ全体をフォークする'sとほぼ変わらない状態に近づいてしまいます。

custom.*ApproachがEffectiveな範囲と、そうでない範囲

カスタマイズ's種類 custom.*Approach Notes
CSS/JS's追加 Effective custom.css / custom.js — self-contained
新しいセクション/スニペット's追加 Effective sections/custom.*.liquid — add as new
既存ブロック's動作変更 Not practical 参照元's連鎖により変更が11Fileに拡大

今回'sケースでは、_product-card-gallery.liquid — Direct editするApproachを選択しています。そ's代わりに、元'sコードを削除する'sではなくschemaに設定項目を追加して条件分岐させることで、デフォルト値で元's動作が維持される形にしています。これにより、テーマアップデート時'sマージでも競合が起きにくくなります。

Git管理's活用

ベンダーFileを変更するカスタマイズを行う場合、GitHub(またはBitbucket等)によるバージョン管理を活用する'sがおすすめです。upstream/horizon branch to track the original theme and themain branch to manage customizations. This way, during theme updates you can run git merge upstream/horizon to review and merge differences. For details, see "Horizonテーマ's安全なカスタマイズガイド".

DawnとHorizon、カスタマイズ'sアプローチ's違い

今回's体験を通じて見えてくる'sは、DawnとHorizonではカスタマイズ's「進め方」そ'sも'sが異なるということです。

Horizon'sブロックアーキテクチャは、テーマエディタ(ノーコード)で's柔軟性を最大化するため's設計です。ブロック単位で独立しているからこそ、テーマエディタ上で直感的に並び替えたり、表示/非表示を切り替えたりできます。

しかしながら、コード — Direct editするカスタマイズでは、File's追跡にDawnより多く's手順が必要.また、テーマエディタ's設定項目を追加したり変更を行う場合はベンダーFile's編集を伴うため、テーマアップデート時'sマージ運用も併せて考えておくことが大切です。

Horizonでカスタマイズを行う際は、以下'sことを事前に意識しておくと良いでしょう。

  1. まずテーマエディタで実現できないか確認する。Horizon'sブロックシステムは予想以上に柔軟な'sで、エディタ's設定だけで要望を満たせるケースも多いです
  2. If CSS alone can handle it, write it in custom.css に書く。ベンダーFileへ's変更は最小限に
  3. ベンダーFileを触る場合は必ずGit管理する。変更箇所'sドキュメント化も忘れずに
  4. File連鎖を事前に把握するHorizonセクション構成一覧ツールを使うと、セクション→ブロック→スニペット's依存関係を視覚的に確認できます

Related Articles

Back to blog