Troubleshooting Horizon theme spotlight section

I’m using the free Shopify Horizon theme and have encountered an issue with the Spotlight section on the homepage. When I hover over the product card, the product link is not clickable, so users can’t open the product page. I’ve attached a screen recording for reference. Could anyone help me identify the cause and suggest a solution?

website: https://barcaan.in/

pw: fds1

@siva_fds ,
This is usually caused by a CSS or JavaScript overlay rather than the Spotlight section itself. A few things I’d check first:

  • Temporarily disable any recently installed apps especially quick view, animations, or popup apps to see if one is blocking the click.
  • Inspect the product card in your browsers Developer Tools and check if another element with a higher z-index is sitting on top of the product link.
  • Switch to an unmodified copy of the Horizon theme and test the Spotlight section there. If it works its likely a customization issue rather than a theme bug.

If the issue only happens on hover i’s almost always an overlapping element intercepting the click events.

I don’t know how to Inspect the product card in your browsers Developer Tools and check if another element with a higher z-index is sitting on top of the product link.

Hi @siva_fds

I checked the issue, but I don’t think it can be fixed with CSS alone. It looks like the JavaScript handling the hover/click behavior will also need to be updated. Could you please share the Collaborator Code so I can investigate further and implement the appropriate fix?

Hey @siva_fds

Here’s a quick workaround:

Follow these Steps:

  1. Go to Online Store
  2. Edit Code
  3. Find theme.liquid file
  4. Add the following code in the bottom of the file above </ body> tag
<style>
@media screen and (min-width: 768px) {
.hotspot .hotspot-dialog[data-placement*=right] {
    left: 50% !important;
}
.hotspot .hotspot-dialog[data-placement*=left] {
    right: 50% !important;
}
}
</style>

RESULT:


Hope that helps! If it did, a Like and Marking it as Solution goes a long way and helps others find the fix faster too.

Best,
Moeed

One option is to update your theme version – they’ve added a fix for this in 4.1.3

This can be problematic though since they’ve also made transition from color schemes to color palettes, which can complicate things.

CSS fix suggested by Moeed works, but is not very stable.

Better fix would be to apply the same Javascript change.

You need to go to “Edit theme code”, find assets/product-hotspot.js and replace its contents from the newer version of this file:

import { Component } from '@theme/component';
import { QuickAddComponent } from '@theme/quick-add';
import { isClickedOutside, isMobileBreakpoint, isTouchDevice, mediaQueryLarge } from '@theme/utilities';

/**
 * A custom element that manages a dialog.
 *
 * @typedef {object} Refs
 * @property {HTMLDialogElement} dialog - The dialog element.
 * @property {HTMLButtonElement} trigger - The button element.
 * @property {HTMLAnchorElement} productLink - The product link element.
 *
 * @extends Component<Refs>
 */

export class ProductHotspotComponent extends Component {
  requiredRefs = ['trigger', 'dialog'];
  /** @type {(() => void) | null} */
  #pointerenterHandler = null;
  timer = /** @type {number | null} */ (null);

  connectedCallback() {
    super.connectedCallback();

    // Set up initial event listeners based on current breakpoint
    this.#handleBreakpointChange();

    // Listen for breakpoint changes
    mediaQueryLarge.addEventListener('change', this.#handleBreakpointChange);
  }

  disconnectedCallback() {
    super.disconnectedCallback();

    // Clean up listeners
    this.#removeDesktopListeners();
    mediaQueryLarge.removeEventListener('change', this.#handleBreakpointChange);
  }

  /**
   * Open the quick-add modal
   * @returns {void}
   */
  #openQuickAddModal() {
    const quickAddComponent = /** @type {QuickAddComponent | null} */ (this.querySelector('quick-add-component'));

    if (!quickAddComponent) return;
    quickAddComponent.handleClick(new MouseEvent('click', { bubbles: true, cancelable: true }));
  }

  /**
   * Set up desktop event listeners (hover)
   * @returns {void}
   */
  #setupDesktopListeners() {
    const { trigger, dialog } = this.refs;

    /** @type {() => void} */
    const pointerenterHandler = () => {
      if (dialog.open) return;

      this.timer = setTimeout(() => {
        this.showDialog();
      }, 120);
      // Add pointerleave listener when entering trigger
      trigger.addEventListener('pointerleave', this.#handlePointerLeave);
    };

    this.#pointerenterHandler = pointerenterHandler;
    trigger.addEventListener('pointerenter', pointerenterHandler);
  }

  /**
   * Remove desktop event listeners from trigger
   * @returns {void}
   */
  #removeDesktopListeners() {
    const { trigger } = this.refs;

    if (this.#pointerenterHandler) {
      trigger.removeEventListener('pointerenter', this.#pointerenterHandler);
      trigger.removeEventListener('pointerleave', this.#handlePointerLeave);
      this.#pointerenterHandler = null;
    }

    // Clear any pending timer
    if (this.timer) {
      clearTimeout(this.timer);
      this.timer = null;
    }
  }

  /**
   * Handle breakpoint changes
   * @returns {void}
   */
  #handleBreakpointChange = () => {
    // Remove existing listeners
    this.#removeDesktopListeners();

    // Set up desktop hover listeners only (mobile uses on:click in template)
    if (!isMobileBreakpoint()) {
      this.#setupDesktopListeners();
    }
  };

  /**
   * Calculate the placement of the dialog.
   * @returns {Promise<void> | undefined}
   */
  #calculateDialogPlacement() {
    const { trigger, dialog } = this.refs;

    const hotspotsContainer = this.parentElement;

    if (!hotspotsContainer) {
      return;
    }

    // Spacing constants
    const BUTTON_GAP = 10; // Gap between button and dialog
    const CONTAINER_GAP = 10; // Gap from container edges
    const TOTAL_GAP = BUTTON_GAP + CONTAINER_GAP;

    // Get container bounds
    const containerRect = hotspotsContainer?.getBoundingClientRect();

    // Get button dimensions
    const triggerRect = trigger.getBoundingClientRect();

    // To get dialog dimensions, we need to temporarily show it invisibly
    // Show dialog invisibly to measure it
    dialog.style.visibility = 'hidden';
    dialog.style.display = 'block';
    dialog.style.transform = 'none';
    dialog.removeAttribute('data-placement');

    const { width: dialogWidth, height: dialogHeight } = dialog.getBoundingClientRect();

    // Reset dialog state
    dialog.style.removeProperty('display');
    dialog.style.removeProperty('visibility');
    dialog.style.removeProperty('transform');
    // Calculate button position relative to container
    const buttonLeft = triggerRect.left - containerRect.left;
    const buttonRight = triggerRect.right - containerRect.left;
    const buttonTop = triggerRect.top - containerRect.top;
    const buttonBottom = triggerRect.bottom - containerRect.top;

    // Calculate available space
    const spaceRight = containerRect.width - buttonRight - CONTAINER_GAP;
    const spaceLeft = buttonLeft - CONTAINER_GAP;

    // Determine horizontal placement
    let x = 'right';

    if (spaceRight >= dialogWidth + BUTTON_GAP) {
      x = 'right';
    } else if (spaceLeft >= dialogWidth + BUTTON_GAP) {
      x = 'left';
    } else {
      x = 'center';
    }

    // Determine vertical placement
    let y = 'bottom';
    let verticalOffset = 0;

    if (x !== 'center') {
      let dialogStartY = buttonTop; // Default to top-aligned
      let dialogEndY = buttonTop + dialogHeight;

      if (dialogEndY > containerRect.height - CONTAINER_GAP) {
        // If top-aligned overflows bottom
        dialogStartY = buttonBottom - dialogHeight;
        dialogEndY = buttonBottom;
        y = 'top';

        if (dialogStartY < CONTAINER_GAP) {
          // If bottom-aligned overflows top
          verticalOffset = CONTAINER_GAP - dialogStartY;
        } else if (dialogEndY > containerRect.height - CONTAINER_GAP) {
          // If bottom-aligned overflows bottom
          verticalOffset = -(dialogEndY - (containerRect.height - CONTAINER_GAP));
        }
      } else {
        if (dialogStartY < CONTAINER_GAP) {
          // If top-aligned overflows top
          if (dialogStartY < CONTAINER_GAP) {
            verticalOffset = CONTAINER_GAP - dialogStartY;
          }
          y = 'bottom';
        }
      }
    } else {
      // For center horizontal: position below or above button
      if (containerRect.height - buttonBottom >= dialogHeight + TOTAL_GAP) {
        y = 'bottom';
      } else if (buttonTop >= dialogHeight + TOTAL_GAP) {
        y = 'top';
      } else {
        // If neither fits well, choose based on button position
        y = buttonTop < containerRect.height / 2 ? 'bottom' : 'top';
      }
    }

    // Set placement data attribute
    dialog.dataset.placement = `${x},${y}`;

    // Apply vertical offset if needed to keep dialog in bounds
    if (verticalOffset !== 0) {
      dialog.style.setProperty('--dialog-vertical-offset', `${verticalOffset}px`);
    } else {
      dialog.style.removeProperty('--dialog-vertical-offset');
    }

    // Return a promise that resolves after a few ticks to ensure styles are applied
    return new Promise((resolve) => setTimeout(resolve, 100));
  }

  /**
   * Handle pointer leave.
   * @param {PointerEvent} e - The event.
   * @returns {void}
   */
  #handlePointerLeave = (e) => {
    const { dialog, trigger } = this.refs;

    // Clear open timer if leaving trigger before dialog opens
    if (this.timer) {
      clearTimeout(this.timer);
      this.timer = null;
    }

    if (!dialog.open) return;

    const isLeavingTrigger = e.target === trigger;
    const isLeavingDialog = e.target === dialog;
    const isGoingToDialog = e.relatedTarget instanceof Element && dialog.contains(e.relatedTarget);
    const isGoingToTrigger = e.relatedTarget === trigger;

    if (isGoingToDialog || (isLeavingDialog && isGoingToTrigger)) return;

    if (isLeavingTrigger || isLeavingDialog) this.closeDialog();
  };

  /**
   * Get the product link for the hotspot product.
   * @returns {HTMLAnchorElement | null} The product link or null.
   */
  getHotspotProductLink() {
    return this.refs.productLink || null;
  }

  /**
   * Handle hotspot click - on mobile/touch devices opens quick-add, on desktop opens dialog
   * @param {MouseEvent} e - The click event
   * @returns {void}
   */
  handleHotspotClick = (e) => {
    // Check if it's a touch device (tablets) or mobile breakpoint
    if (isMobileBreakpoint() || isTouchDevice()) {
      e.preventDefault();
      e.stopPropagation();
      this.#openQuickAddModal();
    } else {
      this.showDialog();
    }
  };

  showDialog = async () => {
    const { dialog } = this.refs;
    await this.#calculateDialogPlacement();
    dialog.dataset.showing = 'true';
    dialog.show();
    document.body.addEventListener('click', this.lightDismissMouse);
    document.body.addEventListener('keydown', this.lightDismissKeyboard);
    document.body.addEventListener('keyup', this.lightDismissKeyboard);
    // Add pointerleave listener to dialog when it opens
    dialog.addEventListener('pointerleave', this.#handlePointerLeave);
  };

  /**
   * Close the dialog.
   * @returns {Promise<void>}
   */
  closeDialog = async () => {
    const { dialog, trigger } = this.refs;
    dialog.dataset.closing = 'true';
    dialog.close();
    document.body.removeEventListener('click', this.lightDismissMouse);
    document.body.removeEventListener('keydown', this.lightDismissKeyboard);
    document.body.removeEventListener('keyup', this.lightDismissKeyboard);
    // Remove pointerleave listeners when closing
    dialog.removeEventListener('pointerleave', this.#handlePointerLeave);
    trigger.removeEventListener('pointerleave', this.#handlePointerLeave);
    // we need to use a data-attribute to keep transition-behavior working only when open
    const animations = dialog.getAnimations({ subtree: true });
    await Promise.allSettled(animations.map((a) => a.finished));
    if (!dialog.open) {
      delete dialog.dataset.showing;
      delete dialog.dataset.closing;
      delete dialog.dataset.placement;
    }
  };

  /**
   * Light dismiss the dialog.
   * @param {MouseEvent} event - The event.
   * @returns {void}
   */
  lightDismissMouse = (event) => {
    const { dialog } = this.refs;
    if (isClickedOutside(event, dialog)) {
      this.closeDialog();
    }
  };

  /**
   * Light dismiss the dialog.
   * @param {KeyboardEvent} event - The event.
   * @returns {void}
   */
  lightDismissKeyboard = (event) => {
    const { dialog } = this.refs;
    if (
      (event.type === 'keydown' && event.key === 'Escape') ||
      (event.type === 'keyup' && !dialog.matches(':is(:focus, :focus-visible, :focus-within)'))
    ) {
      this.closeDialog();
    }
  };
}

// Register custom element
customElements.define('product-hotspot-component', ProductHotspotComponent);

Replace product-hotspot.js code with the above



{% liquid
  assign hotspot_product = closest.product

  assign placeholder_product_title = 'placeholders.product_title' | t
  assign hotspot_product_title = hotspot_product.title | default: placeholder_product_title
%}

<product-hotspot-component
  class="hotspot{% if hotspot_product == blank and request.design_mode == false %} hotspot--hidden-touch hidden--mobile{% endif %}"
  style="
    left: calc({{ block.settings.x-position }}% - var(--button-size) / 2);
    top: calc({{ block.settings.y-position }}% - var(--button-size) / 2);
    --hotspot-bg: {{ section.settings.hotspot_color }};
    --hotspot-bullseye: {{ section.settings.bullseye_color }};
  "
  data-product-url="{{ block.settings.product.url }}"
  data-product-title="{{ block.settings.product.title | escape }}"
  data-id="{{ block.id }}"
  {{ block.shopify_attributes }}
>
  <button
    class="hotspot-trigger"
    aria-label="{{ 'accessibility.show_product_details' | t: product_name: hotspot_product_title }}"
    ref="trigger"
    on:click="/handleHotspotClick"
    style="anchor-name: --hotspot-trigger-{{ block.id }};"
  ></button>
  <dialog
    aria-label="{{ 'accessibility.product_details' | t }}"
    class="hotspot-dialog"
    ref="dialog"
  >
    {% if hotspot_product != blank %}
      <a
        href="{{ hotspot_product.url }}"
        class="hotspot-dialog__link"
        aria-label="{{ hotspot_product.title | escape }}"
        ref="productLink"
      >
      </a>
    {% endif %}
    <div class="hotspot-dialog__product">
      {% if hotspot_product.featured_image != blank %}
        {% render 'image', image: hotspot_product.featured_image, class: 'hotspot-dialog__product-image' %}
      {% else %}
        {{ 'product-apparel-1' | placeholder_svg_tag: 'hotspot-dialog__placeholder-product-image' }}
      {% endif %}
      <div class="hotspot-dialog__product-content">
        <div class="hotspot-dialog__product-info">
          <h2
            class="hotspot-dialog__product-title {{ section.settings.product_title_preset | default: 'h5' }}"
          >
            {{ hotspot_product_title }}
          </h2>
          <div
            class="hotspot-dialog__product-price {{ section.settings.product_price_preset | default: 'h6' }}"
          >
            {% render 'price', product_resource: hotspot_product %}
          </div>
        </div>
        {% if hotspot_product != blank %}
          {% if hotspot_product.available %}
            {% render 'quick-add', product: hotspot_product, section_id: section.id, block: block %}
          {% else %}
            <div class="hotspot-dialog__sold-out-badge">
              {{ 'content.product_badge_sold_out' | t }}
            </div>
          {% endif %}
        {% endif %}
      </div>
    </div>
  </dialog>
</product-hotspot-component>

{% schema %}
{
  "name": "t:names.hotspot_product",
  "tag": null,
  "settings": [
    {
      "type": "product",
      "id": "product",
      "label": "t:settings.product"
    },
    {
      "type": "range",
      "id": "x-position",
      "label": "t:settings.x_position",
      "min": 0,
      "max": 100,
      "step": 1,
      "default": 50
    },
    {
      "type": "range",
      "id": "y-position",
      "label": "t:settings.y_position",
      "min": 0,
      "max": 100,
      "step": 1,
      "default": 50
    }
  ],
  "presets": [
    {
      "name": "t:names.hotspot_product"
    }
  ]
}
{% endschema %}

Replace _hotspot-product.lilquid code with the above

import { Component } from '@theme/component';
import { morph } from '@theme/morph';
import { DialogComponent, DialogCloseEvent } from '@theme/dialog';
import { mediaQueryLarge, isMobileBreakpoint, getIOSVersion } from '@theme/utilities';
import VariantPicker from '@theme/variant-picker';
import { StandardEvents, ProductSelectEvent, CartLinesUpdateEvent } from '@shopify/events';

export class QuickAddComponent extends Component {
  /** @type {AbortController | null} */
  #abortController = null;
  /** @type {Map<string, Element>} */
  #cachedContent = new Map();
  /** @type {AbortController} */
  #cartUpdateAbortController = new AbortController();

  get productPageUrl() {
    const productCard = /** @type {import('./product-card').ProductCard | null} */ (this.closest('product-card'));
    if (productCard) return productCard.productPageUrl;

    const hotspotProduct = /** @type {import('./product-hotspot').ProductHotspotComponent | null} */ (
      this.closest('product-hotspot-component')
    );
    const productLink = hotspotProduct?.getHotspotProductLink();

    return productLink?.href || '';
  }

  /**
   * Gets the currently selected variant ID from the product card
   * @returns {string | null} The variant ID or null
   */
  #getSelectedVariantId() {
    const productCard = /** @type {import('./product-card').ProductCard | null} */ (this.closest('product-card'));
    return productCard?.getSelectedVariantId() ?? null;
  }

  connectedCallback() {
    super.connectedCallback();

    mediaQueryLarge.addEventListener('change', this.#closeQuickAddModal);
    document.addEventListener(StandardEvents.cartLinesUpdate, this.#handleCartUpdate, {
      signal: this.#cartUpdateAbortController.signal,
    });
    document.addEventListener(StandardEvents.productSelect, this.#handleProductSelectUpdate);
  }

  disconnectedCallback() {
    super.disconnectedCallback();

    mediaQueryLarge.removeEventListener('change', this.#closeQuickAddModal);
    this.#abortController?.abort();
    this.#cartUpdateAbortController.abort();
    document.removeEventListener(StandardEvents.productSelect, this.#handleProductSelectUpdate);
  }

  /**
   * Updates quick-add button state when product variant is selected
   * @param {ProductSelectEvent} event - The product select event
   */
  #handleProductSelectUpdate = (event) => {
    if (!(event.target instanceof HTMLElement)) return;
    if (event.target.closest('product-card') !== this.closest('product-card')) return;
    if (this.dataset.usesSellingPlans === 'true') return;

    // Only flip choose <-> add when both buttons were rendered.
    // Otherwise the flip would hide the sole rendered button and reveal nothing.
    if (this.dataset.rendersBothButtons !== 'true') return;

    const productOptionsCount = this.dataset.productOptionsCount;
    const quickAddButton = productOptionsCount === '1' ? 'add' : 'choose';
    this.setAttribute('data-quick-add-button', quickAddButton);
  };

  /**
   * Clears the cached content when cart is updated
   */
  #handleCartUpdate = () => {
    this.#cachedContent.clear();
  };

  /**
   * Re-renders the variant picker in the quick-add modal.
   * @param {Element} newHtml - The element to re-render.
   */
  #updateVariantPicker(newHtml) {
    const modalContent = document.getElementById('quick-add-modal-content');
    if (!modalContent) return;
    const variantPicker = /** @type {VariantPicker | null} */ (modalContent.querySelector('variant-picker'));
    if (!variantPicker) return;
    variantPicker.updateVariantPicker(newHtml);
  }

  /**
   * Handles quick add button click
   * @param {Event} event - The click event
   */
  handleClick = async (event) => {
    event.preventDefault();

    const currentUrl = this.productPageUrl;

    if (this.dataset.usesSellingPlans === 'true') {
      if (currentUrl) window.location.href = currentUrl;
      return;
    }

    // Check if we have cached content for this URL
    let productGrid = this.#cachedContent.get(currentUrl);

    if (!productGrid) {
      // Fetch and cache the content
      const html = await this.fetchProductPage(currentUrl);
      if (html) {
        const gridElement = html.querySelector('[data-product-grid-content]');
        if (gridElement) {
          // Cache the cloned element to avoid modifying the original
          productGrid = /** @type {Element} */ (gridElement.cloneNode(true));
          this.#cachedContent.set(currentUrl, productGrid);
        }
      }
    }

    if (productGrid) {
      // Use a fresh clone from the cache
      const freshContent = /** @type {Element} */ (productGrid.cloneNode(true));
      await this.updateQuickAddModal(freshContent);
      this.#updateVariantPicker(productGrid);
    }

    this.#openQuickAddModal();
  };

  #resetScroll() {
    const dialogComponent = document.getElementById('quick-add-dialog');
    if (!(dialogComponent instanceof QuickAddDialog)) return;

    const productDetails = dialogComponent.querySelector('.product-details');
    const productMedia = dialogComponent.querySelector('.product-information__media');
    productDetails?.scrollTo({ top: 0, behavior: 'instant' });
    productMedia?.scrollTo({ top: 0, behavior: 'instant' });
  }

  /** @param {QuickAddDialog} dialogComponent */
  #stayVisibleUntilDialogCloses(dialogComponent) {
    this.toggleAttribute('stay-visible', true);

    dialogComponent.addEventListener(DialogCloseEvent.eventName, () => this.toggleAttribute('stay-visible', false), {
      once: true,
    });
  }

  #openQuickAddModal = () => {
    const dialogComponent = document.getElementById('quick-add-dialog');
    if (!(dialogComponent instanceof QuickAddDialog)) return;

    this.#stayVisibleUntilDialogCloses(dialogComponent);

    dialogComponent.showDialog();

    // is nondeterministic when the open attribute is set on the dialog element after .showDialog() is called.
    // Waiting until the open animation starts seemed to be the most reliable metric here.
    const dialog = dialogComponent.refs?.dialog;
    if (!dialog) return;
    dialog.addEventListener('animationstart', this.#resetScroll.bind(this), { once: true });
  };

  #closeQuickAddModal = () => {
    const dialogComponent = document.getElementById('quick-add-dialog');
    if (!(dialogComponent instanceof QuickAddDialog)) return;

    dialogComponent.closeDialog();
  };

  /**
   * Fetches the product page content
   * @param {string} productPageUrl - The URL of the product page to fetch
   * @returns {Promise<Document | null>}
   */
  async fetchProductPage(productPageUrl) {
    if (!productPageUrl) return null;

    // We use this to abort the previous fetch request if it's still pending.
    this.#abortController?.abort();
    this.#abortController = new AbortController();

    try {
      const response = await fetch(productPageUrl, {
        signal: this.#abortController.signal,
      });

      if (!response.ok) {
        throw new Error(`Failed to fetch product page: HTTP error ${response.status}`);
      }

      const responseText = await response.text();
      const html = new DOMParser().parseFromString(responseText, 'text/html');

      return html;
    } catch (error) {
      if (error.name === 'AbortError') {
        return null;
      } else {
        throw error;
      }
    } finally {
      this.#abortController = null;
    }
  }

  /**
   * Re-renders the variant picker.
   * @param {Element} productGrid - The product grid element
   */
  async updateQuickAddModal(productGrid) {
    const modalContent = document.getElementById('quick-add-modal-content');

    if (!productGrid || !modalContent) return;

    if (isMobileBreakpoint()) {
      const productDetails = productGrid.querySelector('.product-details');
      const productFormComponent = productGrid.querySelector('product-form-component');
      const variantPicker = productGrid.querySelector('variant-picker');
      const productPrice = productGrid.querySelector('product-price');
      const productTitle = document.createElement('a');
      productTitle.textContent = this.dataset.productTitle || '';

      // Make product title as a link to the product page
      productTitle.href = this.productPageUrl;

      const productHeader = document.createElement('div');
      productHeader.classList.add('product-header');

      productHeader.appendChild(productTitle);
      if (productPrice) {
        productHeader.appendChild(productPrice);
      }
      productGrid.appendChild(productHeader);

      if (variantPicker) {
        productGrid.appendChild(variantPicker);
      }
      if (productFormComponent) {
        productGrid.appendChild(productFormComponent);
      }

      productDetails?.remove();
    }

    // Sync the view-event-payload attribute and morph children into the modal's product-component
    const payload = productGrid.getAttribute('view-event-payload') || '';
    modalContent.setAttribute('view-event-payload', payload);

    morph(modalContent, productGrid);

    this.#syncVariantSelection(modalContent);
  }

  /**
   * Syncs the variant selection from the product card to the modal
   * @param {Element} modalContent - The modal content element
   */
  #syncVariantSelection(modalContent) {
    const selectedVariantId = this.#getSelectedVariantId();
    if (!selectedVariantId) return;

    // Find and check the corresponding input in the modal
    const modalInputs = modalContent.querySelectorAll('input[type="radio"][data-variant-id]');
    for (const input of modalInputs) {
      if (input instanceof HTMLInputElement && input.dataset.variantId === selectedVariantId && !input.checked) {
        input.checked = true;
        input.dispatchEvent(new Event('change', { bubbles: true }));
        break;
      }
    }
  }
}

if (!customElements.get('quick-add-component')) {
  customElements.define('quick-add-component', QuickAddComponent);
}

class QuickAddDialog extends DialogComponent {
  #abortController = new AbortController();

  connectedCallback() {
    super.connectedCallback();

    this.addEventListener(StandardEvents.cartLinesUpdate, this.handleCartUpdate, {
      signal: this.#abortController.signal,
    });
    this.addEventListener(StandardEvents.productSelect, this.#handleProductSelect);

    this.addEventListener(DialogCloseEvent.eventName, this.#handleDialogClose);
  }

  disconnectedCallback() {
    super.disconnectedCallback();

    this.#abortController.abort();
    this.removeEventListener(DialogCloseEvent.eventName, this.#handleDialogClose);
  }

  /**
   * Closes the dialog on successful cart update
   * @param {CartLinesUpdateEvent} event - The cart lines update event
   */
  handleCartUpdate = (event) => {
    event.promise
      ?.then(({ detail }) => {
        if (detail?.didError) return;
        this.closeDialog();
      })
      .catch((error) => {
        if (error?.name !== 'AbortError') console.warn('[quick-add] Event promise rejected:', error);
      });
  };

  /** @param {ProductSelectEvent} event - The product select event */
  #handleProductSelect = (event) => {
    // Wait for variant update data
    event.promise
      .then(({ detail }) => {
        if (!detail?.html) return;

        const { html } = detail;
        const anchorElement = /** @type {HTMLAnchorElement} */ (html.querySelector('.view-product-title a'));
        const viewMoreDetailsLink = /** @type {HTMLAnchorElement} */ (this.querySelector('.view-product-title a'));
        const mobileProductTitle = /** @type {HTMLAnchorElement} */ (this.querySelector('.product-header a'));

        if (!anchorElement) return;

        if (viewMoreDetailsLink) viewMoreDetailsLink.href = anchorElement.href;
        if (mobileProductTitle) mobileProductTitle.href = anchorElement.href;
      })
      .catch((error) => {
        if (error?.name !== 'AbortError') console.warn('[quick-add] Event promise rejected:', error);
      });
  };

  #handleDialogClose = () => {
    const iosVersion = getIOSVersion();
    /**
     * This is a patch to solve an issue with the UI freezing when the dialog is closed.
     * To reproduce it, use iOS 16.0.
     */
    if (!iosVersion || iosVersion.major >= 17 || (iosVersion.major === 16 && iosVersion.minor >= 4)) return;

    requestAnimationFrame(() => {
      /** @type {HTMLElement | null} */
      const grid = document.querySelector('#ResultsList [product-grid-view]');
      if (grid) {
        const currentWidth = grid.getBoundingClientRect().width;
        grid.style.width = `${currentWidth - 1}px`;
        requestAnimationFrame(() => {
          grid.style.width = '';
        });
      }
    });
  };
}

if (!customElements.get('quick-add-dialog')) {
  customElements.define('quick-add-dialog', QuickAddDialog);
}

Replace quick-add.js code with the above

Your site’s source code says that Your theme is not Horizon. It is Atelier 3.4.0. But It is built on Horizon. This is a bug in both the themes.

I tested the fix it on horizon theme in my store. I also injected the given javascript into your site from the frontend, the fix works

Move onto the card and it is gone:

after applying my Fix

Steps

  1. In the theme editor, under Template, click Add section.

  1. Choose custom liquid

  1. Paste the snippet given below into Liquid code, then Save.

<script>
  document.addEventListener('pointerleave', function (event) {
    var dialog = event.target;
    if (!(dialog instanceof HTMLDialogElement)) return;
    if (!dialog.classList.contains('hotspot-dialog')) return;
    // moving onto the card's own child is not "leaving the card"
    if (event.relatedTarget instanceof Node && dialog.contains(event.relatedTarget)) {
      event.stopImmediatePropagation();
    }
  }, true);
</script>

Hey @siva_fds

Hope you’re doing well!

This often cause by a custom code change or an element over lapping the product card and blocking clicks. Try testing the Spotlight section without custom code or third-party apps and inspect the element to see if another layer is covering the product link.