Horizon theme — limit quantity selector to actual stock per variant

Hello,

I use the Horizon theme on Shopify Basic plan.

I need a JavaScript snippet that limits the quantity input/selector on the product page to the actual available stock of the selected variant.

When the customer changes variant, the max quantity should update automatically to match the real stock.

Thank you!

Hervé from Paris

This is a good opportunity to test out Sidekick’s skills. Give it a try and ask it to give you the Javascript for that.

HI @lelongdelajambe

This can be done with JavaScript, but keep in mind that client-side validation is only for convenience. Shopify will still validate inventory server-side at checkout, so the script mainly prevents customers from selecting a quantity higher than the available stock.

The general approach is:

  • Listen for variant changes.
  • Read the selected variant’s inventory_quantity.
  • Set the quantity input’s max attribute to that value.
  • If the customer has already entered a higher quantity, automatically reduce it to the available stock.

Something along these lines:

document.addEventListener('variant:change', (event) => {
  const variant = event.detail.variant;
  const qtyInput = document.querySelector('input[name="quantity"]');

  if (!variant || !qtyInput) return;

  const maxQty = Math.max(1, variant.inventory_quantity);

  qtyInput.max = maxQty;

  if (parseInt(qtyInput.value, 10) > maxQty) {
    qtyInput.value = maxQty;
  }
});

If you’re using the Horizon theme, the exact event name and quantity selector markup may differ from othersr Shopify themes. The snippet may need to be adapted to Horizon’s JavaScript and DOM structure.

If you’re able to share:

  • your product page URL, or
  • the main-product.liquid (or the quantity selector code),

Someone can provide a version that plugs directly into the Horizon theme without additional modifications.

Provide your store URL let review it

“Thank you! I use the Horizon theme. Could you please adapt the snippet specifically for Horizon? The event name and quantity selector may be different from other themes.”

Why not just get it from Sidekick? It really doesn’t take very long…

(function () {
  let variantInventory = {};

  function getProductHandle() {
    if (window.ShopifyAnalytics?.meta?.product?.handle) {
      return window.ShopifyAnalytics.meta.product.handle;
    }
    const match = window.location.pathname.match(/\/products\/([^/?]+)/);
    return match ? match[1] : null;
  }

  function getSelectedVariantId() {
    const urlParams = new URLSearchParams(window.location.search);
    const fromUrl = urlParams.get('variant');
    if (fromUrl) return parseInt(fromUrl, 10);

    const variantSelect = document.querySelector('select[name="id"]');
    if (variantSelect && variantSelect.value) return parseInt(variantSelect.value, 10);

    const hiddenInput = document.querySelector('input[name="id"]');
    if (hiddenInput && hiddenInput.value) return parseInt(hiddenInput.value, 10);

    return null;
  }

  function applyInventoryCap(variantId) {
    const qtyInput = document.querySelector(
      'input[name="quantity"], .quantity__input, input.qty'
    );
    if (!qtyInput) return;

    const max = variantInventory[variantId];

    if (max === null || max === undefined) {
      qtyInput.removeAttribute('max');
    } else {
      qtyInput.setAttribute('max', max);
      if (parseInt(qtyInput.value, 10) > max) {
        qtyInput.value = Math.max(1, max);
      }
    }
  }

  async function fetchInventory(handle) {
    try {
      const res = await fetch(`/products/${handle}.js`);
      const data = await res.json();
      data.variants.forEach((v) => {
        if (
          v.inventory_management === 'shopify' &&
          v.inventory_policy === 'deny'
        ) {
          variantInventory[v.id] = v.inventory_quantity;
        } else {
          variantInventory[v.id] = null;
        }
      });
    } catch (e) {
      console.warn('[InventoryCap] Failed to fetch inventory:', e);
    }
  }

  async function init() {
    const handle = getProductHandle();
    if (!handle) return;

    await fetchInventory(handle);

    // Apply cap on page load
    let variantId = getSelectedVariantId();

    // No variant in URL — fall back to first variant in inventory map
    if (!variantId) {
      variantId = parseInt(Object.keys(variantInventory)[0], 10);
    }

    if (variantId) applyInventoryCap(variantId);

    // Horizon custom event (swatches, dropdowns)
    document.addEventListener('variant:change', (e) => {
      const id = e.detail?.variant?.id || getSelectedVariantId();
      if (id) applyInventoryCap(id);
    });

    // Fallback for native select-based variant pickers
    const variantSelect = document.querySelector('select[name="id"]');
    if (variantSelect) {
      variantSelect.addEventListener('change', () => {
        applyInventoryCap(parseInt(variantSelect.value, 10));
      });
    }
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init);
  } else {
    init();
  }
})();

{% if template == 'product' %}
  {{ 'inventory-cap.js' | asset_url | script_tag }}
{% endif %}

I built and tested in horizon store.

Problem

Fix

Add the code, in a Custom Liquid block on the product page

  • Online Store, Themes, Customize.
  • Open a product, and in the Product information area click Add block, then Custom Liquid.
  • Paste the code below into the Liquid code box, then Save.

{%- comment -%} Limit the quantity to the selected variant's stock {%- endcomment -%}
<script>
(function () {
  var STOCK = {
    {%- assign first = true -%}
    {%- for v in product.variants -%}
      {%- if v.inventory_management != blank and v.inventory_policy != 'continue' and v.inventory_quantity > 0 -%}
        {%- assign cap = v.inventory_quantity -%}
        {%- if v.quantity_rule.max != blank and v.quantity_rule.max < cap -%}{%- assign cap = v.quantity_rule.max -%}{%- endif -%}
        {%- unless first -%},{%- endunless -%}"{{ v.id }}": {{ cap }}{%- assign first = false -%}
      {%- endif -%}
    {%- endfor -%}
  };
  function variantId() {
    var el = document.querySelector('product-form-component input[name="id"], form[action*="/cart/add"] input[name="id"], input[name="id"]');
    return el ? el.value : null;
  }
  function apply() {
    var id = variantId();
    document.querySelectorAll('quantity-selector-component').forEach(function (qs) {
      var input = qs.querySelector('input[name="quantity"]');
      if (!input) return;
      var min = input.getAttribute('min') || '1';
      var step = input.getAttribute('step') || '1';
      var cap = STOCK.hasOwnProperty(id) ? String(STOCK[id]) : null;
      if (typeof qs.updateConstraints === 'function') qs.updateConstraints(min, cap, step);
      else if (cap) { input.setAttribute('max', cap); if (parseInt(input.value, 10) > +cap) input.value = cap; }
      else input.removeAttribute('max');
    });
  }
  document.addEventListener('shopify:product:select', function (e) {
    if (e && e.promise && e.promise.then) e.promise.then(apply, apply);
    setTimeout(apply, 350);
  });
  if (document.readyState !== 'loading') apply();
  else document.addEventListener('DOMContentLoaded', apply);
})();
</script>

The quantity now stops at that variant’s stock, the plus button greys out at the limit, and it re-caps every time the shopper changes variant.

Note

  • The block sits in the product template, so it covers every product on that template.
  • Sold-out variants are already blocked by the Add to cart button, so those are left alone.
  • If a variant allows overselling, it stays unlimited on purpose.

Limitations:

Note: the “up to 5 in stock” (for example) limit is fixed when the page loads. If someone else buys some of that variant meanwhile, your page still lets you pick up to 5 until you refresh. Shopify catches it at checkout, so you can’t oversell, but the on-page number can be stale until then.

And there is also caching layer to worry about

“Thank you! I use the Horizon theme. Could you please adapt the snippet specifically for Horizon? The event name and quantity selector may be different from other themes.”

Thank a lot, good solution with liquid bloc in product not in general code, easy to do.

Hervé

Sidekick doesn’t know to adapt for Horizon, the solution by ajaycodewiz is perfect !

But i tested it on horizon. What errors are u getting ?

No problem indeed. I have open a new topic for clear the not available variantes. Thanks again.