Collection product cards

Hello everyone,

I’m using the Shopify Horizon theme and I’m trying to add a custom visual price line under the existing AED price on collection product cards.

I’m attaching a screenshot (see red arrow) and explaining exactly where the price appears and what we tried, so you can see where the correct hook/placement should be.


1) Where the price is displayed (important)

On collection pages, each product card shows the price like this:

Dhs. 210.00

This price appears:

  • Inside a product card (grid item)

  • Rendered by Horizon’s Web Components (product-card, product-price)

  • NOT as a simple .price span like classic themes

The red arrow in the screenshot points to the exact visual location:
:backhand_index_pointing_right: Directly under the product title, inside the product card


2) What I want to add (visual only)

After

that existing price, I want to show:

≈ USD 57

This is:

  • Visual only (no checkout impact)

  • Collection page only

  • Not touching product templates or checkout


3) JavaScript file created

I created a custom JS file:

Path

/assets/AED-USD.js


4) Code placed inside AED-USD.js (exact)

console.log('AED-USD.js LOADED');

(() => {
  const AED_TO_USD = 0.27;

  function scan() {
    document.querySelectorAll('product-card').forEach(card => {
      if (card.querySelector('.cm-usd-line')) return;

      const text = card.innerText || '';
      const match = text.match(/Dhs\.\s*([0-9,.]+)/i);
      if (!match) return;

      const aed = parseFloat(match[1].replace(/,/g, ''));
      if (!aed) return;

      const usd = Math.round(aed * AED_TO_USD);
      if (!usd) return;

      const el = document.createElement('div');
      el.className = 'cm-usd-line';
      el.textContent = `≈ USD ${usd}`;
      el.style.fontSize = '12px';
      el.style.color = '#777';

      card.appendChild(el);
    });
  }

  document.addEventListener('DOMContentLoaded', scan);
})();


5) How the file is loaded

In layout/theme.liquid, right before </body>, I added:

<script src="{{ 'AED-USD.js' | asset_url }}" defer></script>

Full context at the bottom of the file:

{% if settings.quick_add or settings.mobile_quick_add %}
  {% render 'quick-add-modal' %}
{% endif %}

<script src="{{ 'AED-USD.js' | asset_url }}" defer></script>
</body>
</html>


6) Result

:cross_mark: The JS file does not execute:

  • No console.log

  • No alert

  • No DOM changes

Everything else in the theme works normally.


7) Core question (what I need help with)

Given Horizon’s architecture:

  • Web Components

  • import maps

  • modular scripts loaded via snippets/scripts.liquid

:backhand_index_pointing_right: Where is the correct and supported place to hook custom logic that targets product-card pricing?

Specifically:

  • Should this be registered as a module inside snippets/scripts.liquid?

  • Is there a Horizon lifecycle / event for product card hydration?

  • Is layout/theme.liquid too late or ignored for this purpose?

Any guidance from someone who has modified Horizon product cards would be greatly appreciated.

Thank you.

1 Like

It would be ideal if you can share a preview link to your work in progress theme.

Note that your script executes only once, but product grid is reloaded after every filter change and page change/infinite scroll.

Doing this with JS would not be best also because it will cause a big CLS-> break your PageSpeed.

One of the bigger Horizon benefits is that the product cards are configurable in “Theme Edit”.
I’d rather add a “Custom liquid” block inside your Product Card block with the code like:

<div class="cm-usd-line">
  ≈ USD ${{ closest.product.price | times: 0.27 | money }}
</div>
1 Like

Thank you very much for your help

In short layout/theme.liquid is the correct place, but your logic is failing because Horizon’s Web Components render asynchronously.

Your script runs immediately on DOMContentLoaded, but the product-card elements and the prices inside them are likely not fully rendered or hydrated at that exact millisecond.

Here is the solution to fix both the loading and the rendering timing:

  1. Fix the loading (no console log).
    If you see no log, the asset path might be resolving incorrectly or cached. To guarantee execution, move the code inline inside layout/theme.liquid right before the tag (instead of calling the .js file).
  2. Fix the logic (MutationObserver)
    Since horizon loads products dynamically and via infinite scroll you must use a MutationObserver instead of a one-time scan. Replace your current script implementation with the provided below.
<script>
  console.log('AED-USD script started');

  (function() {
    const AED_TO_USD = 0.27;
    function addUsdPrice(card) {
      if (card.dataset.usdAdded) return;
      requestAnimationFrame(() => {
        const text = card.innerText || '';
        const match = text.match(/Dhs\.\s*([0-9,.]+)/i); 
        
        if (match) {
          const aed = parseFloat(match[1].replace(/,/g, ''));
          if (!isNaN(aed)) {
            const usd = Math.round(aed * AED_TO_USD);
            
            const el = document.createElement('div');
            el.className = 'cm-usd-line';
            el.innerHTML = `≈ USD ${usd}`;
            el.style.cssText = 'font-size: 12px; color: #777; margin-top: 4px;';
            card.appendChild(el);
            card.dataset.usdAdded = 'true';
          }
        }
      });
    }
    document.querySelectorAll('product-card').forEach(addUsdPrice);
    const observer = new MutationObserver((mutations) => {
      mutations.forEach((mutation) => {
        mutation.addedNodes.forEach((node) => {
          if (node.nodeType === 1) {
            if (node.tagName === 'PRODUCT-CARD') {
              addUsdPrice(node);
            } 
            else if (node.querySelectorAll) {
              node.querySelectorAll('product-card').forEach(addUsdPrice);
            }
          }
        });
      });
    });

    observer.observe(document.body, { childList: true, subtree: true });
  })();
</script>
1 Like

Hi friend,

I just wanted to sincerely thank you for your help. Your code worked perfectly and solved the issue exactly as I needed.

Your guidance was clear, precise, and incredibly helpful. I truly appreciate the time and effort you put into your response.

Thanks again — you made a real difference! :folded_hands:

I’m happy that it’s work for you.

1 Like

Hello friend,

I’m currently working with a Shopify theme where the product description typography looks oversized and inconsistent across different products. The font sizes inside the description area appear too large and visually unbalanced, which affects the overall design and readability of the product page.

I would like to know if there is a clean and reliable way to globally standardize all product descriptions at once — for example:

  • Normalize font sizes

  • Ensure consistent heading spacing

  • Apply justified text alignment

  • Remove or override inline font-size styles

  • Keep the theme’s default typography system intact

Ideally, I’m looking for a solution that can be added at the theme level (Liquid or CSS), so that all existing and future product descriptions are automatically formatted in a clean and professional way without editing each product individually.

Is this best handled via a global CSS override, a modification inside the text snippet, or another recommended approach?

Any guidance or best practices would be greatly appreciated.

Thank you in advance.